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

Knowledge Overview

About Me

I’m Ngo Quang Huy, a Software Engineer based in Hanoi, Vietnam. I have 4 years of professional experience building scalable, enterprise-level systems using .NET Core, SQL Server, and ReactJS — with a strong focus on Backend development.

My journey started at FPT Software, where I worked on a SaaS pension management system in the financial domain. As a Backend Engineer, I handled member management, payroll calculation, and report generation. For my contributions, I received the Best Performer award in 2024. After that, I joined another project at FPT Software, building a Monitoring and Alert System powered by Elasticsearch data.

Currently, I work at Optimizely — a Content Management System — as a Fullstack Engineer, though most of my work is still backend-focused. My main responsibility is customizing the CMS platform based on customer requirements.

I’m now looking for new opportunities where I can apply my backend skills, grow as an engineer, and take on new challenges. I believe that every project is a chance to improve, and I’m always ready to contribute my best. Glad to see with everyone, and I hope we have a great conversation ahead!


Giới thiệu

Tài liệu này tổng hợp kiến thức kỹ thuật theo lộ trình học tập dành cho Backend Developer, bao gồm 7 mảng công nghệ chính. Mỗi mảng được tổ chức từ nền tảng đến nâng cao, kèm code example thực tế.


Danh sách chủ đề

#Mảng kiến thứcCông nghệSố chủ đề
1SQL ServerT-SQL, Index, Transactions, HA6
2C#/.NETASP.NET Core, EF Core, Architecture9
3ReactJSReact 18+, Hooks, Redux, Next.js14
4ElasticsearchSearch, Aggregations, .NET Client9
5Work ExperienceOptimizely, Pension System, Monitoring4
6AlgorithmsProblem Solving, Data Structures2
7System DesignArchitecture, Scalability, Case Studies7

1. SQL Server

Kiến thức SQL Server từ nền tảng đến các tính năng doanh nghiệp.

#Chủ đềNội dung chính
1Nền Tảng SQL ServerT-SQL cơ bản, kiến trúc, kiểu dữ liệu, DDL, DML, Joins
2Index & Hiệu suấtClustered/Non-Clustered Index, Execution Plans, Query Optimization
3Lập trình T-SQLStored Procedures, Functions, Triggers, Views, CTEs, Window Functions
4Giao dịch & Đồng thờiACID, Isolation Levels, Locking, Blocking, Deadlocks
5Bảo mật & Quản trịAuthentication, Permissions, Backup & Recovery, Agent Jobs, Monitoring
6Tính năng Nâng caoPartitioning, JSON/XML, Temporal Tables, In-Memory OLTP, High Availability, CDC

2. C#/.NET

Lộ trình đầy đủ cho vị trí C#/.NET Developer, từ ngôn ngữ đến kiến trúc hệ thống.

#Chủ đềNội dung chính
1Nền Tảng C# và .NETNgôn ngữ C#, CLR, GC, Value vs Reference Types, Async/Await, LINQ
2ASP.NET Core Cốt lõiMiddleware, DI, Routing, Filters, Kestrel, SignalR, gRPC
3Xây dựng Web APIRESTful API, Authentication/Authorization, Versioning, Swagger
4Truy cập Dữ liệu với EF CoreCode First, Migrations, N+1 Query, Transactions, Concurrency
5Kiến trúc Phần mềmSOLID, Design Patterns, Clean Architecture, DDD, CQRS, Event-Driven
6Hiệu suất và Bất đồng bộCaching, Rate Limiting, Load Handling, Async Processing
7Hệ thống Phân tánMessage Queue, Azure Service Bus, Docker, Kubernetes
8Kiểm thửUnit Test, Integration Test, xUnit, Mocking
9Câu hỏi Phân biệtSo sánh các công nghệ và concepts thường gặp trong phỏng vấn

Kiến trúc Phần mềm (Chi tiết)

Xem chi tiết các pattern và kiến trúc

Nguyên tắc Thiết kế

Design Patterns

Kiến trúc Ứng dụng

Kiến trúc Hệ thống

Phương pháp Phát triển

  • TDD — Test-Driven Development
  • BDD — Behavior-Driven Development

3. ReactJS

React 18+ với functional components và hooks. Bao gồm state management, data fetching, và Next.js.

#Chủ đềNội dung chính
1JSX & RenderingJSX syntax, Virtual DOM, Conditional rendering, List rendering
2Components & PropsFunctional components, Props, Children, Composition
3Hooks Cơ bảnuseState, useEffect, useRef, useId
4Hooks Nâng caouseReducer, useMemo, useCallback, useLayoutEffect, Custom Hooks
5Context APIcreateContext, useContext, Provider pattern
6Redux & Redux ToolkitStore, Slice, Thunk, RTK Query
7React QueryData fetching, Caching, Mutations, Pagination
8React RouterRoutes, Navigation, Nested routes, Protected routes
9Forms & ValidationControlled forms, React Hook Form, Zod validation
10StylingCSS Modules, styled-components, Tailwind CSS
11PerformanceMemo, Code splitting, Lazy loading, Profiler
12TestingJest, React Testing Library, Mock API
13React PatternsHOC, Render Props, Compound Components, Portals
14Next.js Cơ bảnSSR, SSG, ISR, App Router, Server Components

4. Elasticsearch

Elasticsearch 8+ với .NET client chính thức (Elastic.Clients.Elasticsearch).

#Chủ đềNội dung chính
1Concepts Cơ bản & Kết nốiIndex, Shard, Replica, DI setup trong ASP.NET Core
2Mapping & Field TypesAttribute mapping, Fluent mapping, text vs keyword, nested
3Indexing DocumentsCRUD, Bulk API, Upsert, Ingest Pipeline
4Basic SearchMatch, Term, Range, Bool query, Sorting
5Query DSL Nâng caoMulti-match, Fuzzy, Nested query, Highlight, Scroll
6AggregationsMetric, Terms, Range, Date Histogram, Faceted Search
7Analyzers & TokenizersBuilt-in analyzers, Custom analyzer, Autocomplete
8Performance TuningFilter vs must, Source filtering, Search After, ILM
9Cluster ManagementHealth check, ILM, Alias, Snapshot, ASP.NET Core integration

5. Work Experience

Real-world projects and professional experience in enterprise software development.

#ProjectRoleKey Achievements
1KF Project - OptimizelyFullstack DeveloperGoogle Maps integration, CMS configuration, cross-functional Agile team
2SKCC Project - FPTBackend DeveloperWPF monitoring app, webhook engine, REST APIs, ElasticSearch integration
3PTG.PPPlus3 - Pension SystemBackend DeveloperReport generation (-50% time), calculation optimization (-90% time), message queues
4Interview Q&ACommon interview questions and answers from real projects

Key Achievements

  • Performance Optimization: Reduced pension calculation time by 90% through event-driven architecture and pre-calculation strategies.
  • Report Generation: Cut report processing time by 50% using background jobs, message queues, and pre-generated JSON files.
  • Fullstack Delivery: Led Google Maps integration in Optimizely CMS, bridging backend configuration with rich frontend UX.
  • Recognition: Best Performer 2024 at FPT Software for backend optimization contributions.

6. Algorithms

Fundamental algorithms and data structures for problem-solving and technical interviews.

#TopicDescription
1Linear Equation SolverSolve ax + b = 0 equations
2Best Time to Buy/Sell StockMaximize profit from stock price array

7. System Design

Scalable system design principles, architectural patterns, and real-world case studies.

#TopicDescription
1Basic PrinciplesScalability, reliability, availability, efficiency
2System ComponentsLoad balancers, caches, databases, message queues
3Architectural PatternsMonolith, microservices, event-driven, serverless
4Design MethodologyRequirements gathering, estimation, component design
5Processing TechniquesSharding, replication, partitioning, rate limiting
6Case StudiesReal-world system designs:
- URL Shortener
- Chat Application
- Social Media Feed
- E-commerce Platform
- Ride-sharing
- Video Streaming
7Interview QuestionsCommon system design interview questions

Lộ trình học gợi ý

Cho người mới bắt đầu

  1. C# Cơ bảnOOPAsync/Await & LINQ
  2. SQL Server Nền tảngIndex & Hiệu suất
  3. JSX & RenderingHooks Cơ bản

Cho .NET Developer

  1. SOLID PrinciplesDesign PatternsClean Architecture
  2. EF CoreCQRSMicroservices
  3. Elasticsearch Core ConceptsQuery DSL

Chuẩn bị phỏng vấn

SQL Server Roadmap

Chương này bao gồm toàn bộ kiến thức SQL Server từ cơ bản đến nâng cao, phù hợp cho mọi cấp độ từ Junior đến Senior.

Lộ trình học

1. Nền Tảng SQL Server

Kiến trúc, T-SQL cơ bản, kiểu dữ liệu, DDL, DML và truy vấn.

2. Index & Hiệu suất

Clustered/Non-Clustered Index, Execution Plans, Query Optimization, Statistics.

3. Lập trình T-SQL

Stored Procedures, Functions, Triggers, Views, CTEs, Window Functions.

4. Giao dịch & Đồng thời

ACID, Isolation Levels, Locking, Blocking, Deadlocks.

5. Bảo mật & Quản trị

Authentication, Permissions, Backup & Recovery, Agent Jobs, Monitoring.

6. Tính năng Nâng cao

Partitioning, JSON/XML, Temporal Tables, In-Memory OLTP, High Availability, CDC.

1. Nền Tảng SQL Server

Giới thiệu

Phần này trình bày các kiến thức nền tảng về SQL Server và T-SQL, từ kiến trúc hệ thống đến các câu lệnh cơ bản nhất. Đây là nền tảng bắt buộc để hiểu sâu hơn về hiệu suất, indexing và các chủ đề nâng cao.

Nội dung chính

Kiến trúc SQL Server

Kiểu dữ liệu

  • Kiểu Dữ liệu - Numeric, String, DateTime, Binary types; Unicode vs non-Unicode; type conversion

DDL - Data Definition Language

  • DDL - CREATE/ALTER/DROP cho database objects, constraints, identity, sequences, temporal tables, indexes

DML - Data Manipulation Language

  • DML - INSERT, UPDATE, DELETE, MERGE, BULK operations, OUTPUT clause, transactions

Câu hỏi phỏng vấn (Sắp xếp từ dễ đến khó)

Mức độ: Dễ (Junior)

  1. SQL Server là gì? Sự khác biệt giữa SQL Server instance và database là gì?
  2. Các file database trong SQL Server có những loại nào? (.mdf, .ndf, .ldf)
  3. SELECT, WHERE, ORDER BY, GROUP BY khác nhau như thế nào về mục đích sử dụng?
  4. HAVINGWHERE khác nhau như thế nào?
  5. Sự khác biệt giữa INNER JOIN, LEFT JOIN, RIGHT JOIN, và FULL OUTER JOIN?
  6. NULL trong SQL Server là gì? Cách xử lý NULL với IS NULL, ISNULL(), COALESCE()?
  7. DISTINCT dùng để làm gì? Ví dụ thực tế?
  8. Sự khác biệt giữa charvarchar? Giữa varcharnvarchar?
  9. int, bigint, smallint, tinyint khác nhau ở điểm gì?
  10. PRIMARY KEYUNIQUE constraint khác nhau như thế nào?
  11. NOT NULL constraint hoạt động như thế nào?
  12. DEFAULT constraint dùng để làm gì?
  13. Câu lệnh INSERT INTO...VALUES cơ bản viết như thế nào?
  14. Sự khác biệt giữa DELETETRUNCATE TABLE?
  15. IDENTITY column là gì? Cú pháp khai báo như thế nào?

Mức độ: Trung bình (Mid-level)

  1. CASE expression (simple và searched) hoạt động như thế nào? Ví dụ thực tế?
  2. TOP NOFFSET-FETCH khác nhau như thế nào? Cái nào nên dùng cho phân trang?
  3. Các hàm xử lý chuỗi phổ biến: LEN, SUBSTRING, CHARINDEX, REPLACE, TRIM?
  4. Sự khác biệt giữa GETDATE(), GETUTCDATE(), SYSDATETIME(), và SYSUTCDATETIME()?
  5. DATEADD, DATEDIFF, DATEPART dùng như thế nào?
  6. decimal vs float vs money - khi nào dùng cái nào? Tại sao không nên dùng float cho tiền tệ?
  7. datetime vs datetime2 vs datetimeoffset - sự khác biệt và khi nào dùng?
  8. CAST vs CONVERT vs TRY_CAST vs TRY_CONVERT - khác nhau như thế nào?
  9. FOREIGN KEY constraint là gì? ON DELETE CASCADE hoạt động như thế nào?
  10. CHECK constraint dùng để làm gì? Ví dụ thực tế?
  11. UPDATE với JOIN viết như thế nào?
  12. DELETE với JOIN viết như thế nào?
  13. MERGE statement (upsert) dùng để làm gì? Cú pháp cơ bản?
  14. OUTPUT clause trong DML dùng để làm gì?
  15. Computed column là gì? Sự khác biệt giữa persisted và non-persisted?
  16. Schemas trong SQL Server là gì? Tại sao nên dùng schemas thay vì tất cả trong dbo?

Mức độ: Khó (Senior)

  1. SQL Server Buffer Pool hoạt động như thế nào? Tại sao memory management quan trọng?
  2. PagesExtents trong SQL Server là gì? Ảnh hưởng đến hiệu năng như thế nào?
  3. Plan Cache hoạt động như thế nào? Parameter sniffing là gì và khi nào gây ra vấn đề?
  4. Implicit conversion trong SQL Server là gì? Tại sao có thể gây ra index scan thay vì seek?
  5. Temporal Tables (system-versioned) là gì? Use case thực tế?
  6. SEQUENCE object so với IDENTITY column - ưu nhược điểm?
  7. NEWID() vs NEWSEQUENTIALID() vs IDENTITY - khi nào dùng cái nào? Tác động đến index fragmentation?
  8. BULK INSERT vs INSERT INTO...SELECT - khi nào dùng cái nào cho hiệu năng tốt nhất?
  9. Set-based operations vs row-by-row (cursor) - vì sao nên tránh cursor trong SQL Server?
  10. nvarchar(MAX) vs nvarchar(4000) - tác động đến hiệu năng và storage?
  11. INDEX trong DDL - Clustered vs Nonclustered, khi nào tạo index trong CREATE TABLE và khi nào tách riêng?
  12. Cascading referential actions (CASCADE, SET NULL, NO ACTION) - tác động đến performance với large datasets?

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;

Kiểu Dữ liệu trong SQL Server

1. Kiểu số (Numeric Types)

Integer Types

KiểuBytesGiá trị minGiá trị maxDùng khi
tinyint10255Cột nhỏ như status, age (0-120)
smallint2-32,76832,767ID nhỏ, year
int4-2,147,483,6482,147,483,647FK, PK cho hầu hết tables
bigint8-9.2 × 10¹⁸9.2 × 10¹⁸ID lớn, counts, Unix timestamps
-- Ví dụ sử dụng
CREATE TABLE Products (
    ProductId    INT          NOT NULL,  -- PK thông thường
    CategoryId   TINYINT      NOT NULL,  -- Giả sử < 256 categories
    StockCount   SMALLINT     NOT NULL,  -- Tồn kho không quá 32767
    TotalOrders  BIGINT       NOT NULL DEFAULT 0  -- Có thể rất lớn
);

-- Tính toán có thể overflow nếu dùng sai type
SELECT 2000000000 + 2000000000;           -- Overflow với INT!
SELECT CAST(2000000000 AS BIGINT) + 2000000000;  -- OK

-- Kiểm tra giới hạn
SELECT 
    'tinyint'  AS type_name, CAST(255 AS TINYINT) AS max_val UNION ALL
    SELECT 'smallint', CAST(32767 AS SMALLINT) UNION ALL
    SELECT 'int',      CAST(2147483647 AS INT) UNION ALL
    SELECT 'bigint',   CAST(9223372036854775807 AS BIGINT);

Decimal/Numeric Types

DECIMAL(p, s)NUMERIC(p, s) là synonyms, lưu số thập phân chính xác:

  • p (precision): tổng số chữ số (1-38)
  • s (scale): số chữ số sau dấu thập phân (0 đến p)
PrecisionStorage
1-95 bytes
10-199 bytes
20-2813 bytes
29-3817 bytes
-- Dùng cho tiền tệ và số thập phân cần chính xác
DECLARE @price DECIMAL(10, 2);  -- Tối đa 99999999.99
DECLARE @rate  DECIMAL(5, 4);   -- Tối đa 9.9999 (ví dụ: tax rate)
DECLARE @qty   NUMERIC(18, 3);  -- Lượng với 3 chữ số thập phân

-- Ví dụ tính toán chính xác
SELECT CAST(0.1 AS DECIMAL(10,1)) + CAST(0.2 AS DECIMAL(10,1));  -- 0.3 chính xác
SELECT 0.1 + 0.2;  -- 0.300000000000... (float, không chính xác)

Float và Real

KiểuBytesKý hiệu khoa họcChính xác
real4~7 chữ sốApproximate
float8~15 chữ sốApproximate
float(n)4 hoặc 8n = 1-53Approximate

Cảnh báo: floatrealapproximate - KHÔNG dùng cho tiền tệ, kế toán, hoặc bất kỳ tính toán nào cần độ chính xác tuyệt đối!

-- Vấn đề với float
SELECT CAST(0.1 AS FLOAT) + CAST(0.2 AS FLOAT);
-- Kết quả: 0.30000000000000004 (sai!)

-- Float thích hợp cho: tọa độ GPS, đo lường khoa học, phân tích thống kê
CREATE TABLE SensorData (
    ReadingId  INT PRIMARY KEY,
    Latitude   FLOAT NOT NULL,   -- GPS coordinates
    Longitude  FLOAT NOT NULL,
    Temperature REAL NOT NULL    -- Nhiệt độ, sai số nhỏ chấp nhận được
);

Money Types

KiểuBytesPhạm viChính xác
money8±922 trillion4 decimal places
smallmoney4±214,7484 decimal places
-- money type
DECLARE @price MONEY = 19.99;
DECLARE @tax   MONEY = 0.10;
SELECT @price + @tax;  -- 20.09

-- Vấn đề với money: phép chia có thể mất độ chính xác
DECLARE @total MONEY = 100;
SELECT @total / 3;  -- 33.3333 (OK)
SELECT @total / 3 * 3;  -- 99.9999 (mất 0.0001!)

-- Khuyến nghị: dùng DECIMAL(19, 4) thay vì MONEY để có kiểm soát tốt hơn

2. Kiểu chuỗi (String Types)

Non-Unicode vs Unicode

KiểuEncodingbytes/ký tựTối đa
char(n)Non-Unicode (ASCII)18,000 ký tự
varchar(n)Non-Unicode (ASCII)18,000 ký tự
varchar(MAX)Non-Unicode12 GB
nchar(n)Unicode (UTF-16)24,000 ký tự
nvarchar(n)Unicode (UTF-16)24,000 ký tự
nvarchar(MAX)Unicode22 GB

char vs varchar

-- char(n): fixed-length, padding bằng spaces nếu ngắn hơn n
DECLARE @fixed CHAR(10) = 'Hello';
SELECT LEN(@fixed);    -- 5 (LEN bỏ trailing spaces)
SELECT DATALENGTH(@fixed);  -- 10 (luôn 10 bytes)

-- varchar(n): variable-length, không padding
DECLARE @var VARCHAR(10) = 'Hello';
SELECT LEN(@var);       -- 5
SELECT DATALENGTH(@var); -- 5

-- Khi nào dùng char: dữ liệu có độ dài cố định (mã code, country code)
CREATE TABLE Countries (
    CountryCode CHAR(2) NOT NULL,  -- Luôn đúng 2 ký tự: 'US', 'VN', 'JP'
    PhoneCode   CHAR(4),           -- '+84 ', '+1  '
    CountryName VARCHAR(100) NOT NULL
);

nvarchar vs varchar - Unicode

-- Dùng N prefix để tạo Unicode string literal
INSERT INTO Employees (FullName) VALUES (N'Nguyễn Văn An');  -- Đúng với dấu
INSERT INTO Employees (FullName) VALUES ('Nguyễn Văn An');   -- Có thể sai nếu column là nvarchar

-- Khi nào dùng nvarchar:
-- - Dữ liệu nhiều ngôn ngữ (tiếng Việt, tiếng Trung, Arabic, etc.)
-- - Tên người, địa chỉ
-- - Nội dung do user nhập

-- Khi nào dùng varchar:
-- - Codes, IDs, email addresses (ASCII-safe)
-- - Dữ liệu hệ thống không cần Unicode
-- - Tiết kiệm storage khi biết chắc chỉ dùng ASCII

-- So sánh storage
CREATE TABLE StorageDemo (
    col_varchar  VARCHAR(100)  = 'Hello',
    col_nvarchar NVARCHAR(100) = N'Hello'
);
-- varchar: 5 bytes
-- nvarchar: 10 bytes (2 bytes/ký tự)

varchar(MAX) và nvarchar(MAX)

-- Dùng cho dữ liệu text rất lớn (tới 2GB)
CREATE TABLE Articles (
    ArticleId INT PRIMARY KEY,
    Title     NVARCHAR(500) NOT NULL,
    Content   NVARCHAR(MAX),           -- Nội dung dài
    HtmlContent VARCHAR(MAX)           -- HTML content (ASCII)
);

-- Chú ý hiệu năng với MAX:
-- - Không thể là key trong index
-- - Storage khác: có thể lưu inline (nếu < 8000 bytes) hoặc LOB pages
-- - Dùng .WRITE() để update hiệu quả
UPDATE Articles
SET Content.WRITE(N' Thêm nội dung', DATALENGTH(Content)/2, 0)
WHERE ArticleId = 1;

text và ntext (Deprecated)

-- KHÔNG dùng nữa từ SQL Server 2005+
-- text: tương tự varchar(MAX) nhưng deprecated
-- ntext: tương tự nvarchar(MAX) nhưng deprecated

-- Nếu thấy trong code cũ, convert sang varchar(MAX)/nvarchar(MAX):
ALTER TABLE OldTable ALTER COLUMN OldTextCol NVARCHAR(MAX);

3. Kiểu ngày giờ (Date/Time Types)

KiểuBytesPhạm viĐộ chính xácTimezone
datetime81753-01-01 ~ 9999-12-313.33msKhông
smalldatetime41900-01-01 ~ 2079-06-061 phútKhông
datetime2(n)6-80001-01-01 ~ 9999-12-31100nsKhông
date30001-01-01 ~ 9999-12-311 ngàyKhông
time(n)3-500:00:00 ~ 23:59:59.9…100nsKhông
datetimeoffset(n)8-100001-01-01 ~ 9999-12-31100nsCó (+/-14h)
-- datetime - loại cũ, tránh dùng cho code mới
DECLARE @dt DATETIME = '2026-04-01 10:30:00.000';

-- datetime2 - khuyến nghị thay datetime
DECLARE @dt2 DATETIME2(7) = '2026-04-01 10:30:00.1234567';  -- 7 decimal seconds
DECLARE @dt2s DATETIME2(0) = '2026-04-01 10:30:00';         -- Chỉ đến giây

-- date - chỉ cần ngày, không cần giờ
DECLARE @d DATE = '2026-04-01';
DECLARE @birthDate DATE = '1990-05-15';

-- time - chỉ cần giờ
DECLARE @t TIME(0) = '10:30:00';     -- Đến giây
DECLARE @t7 TIME(7) = '10:30:00.1234567';  -- Cao nhất

-- datetimeoffset - khi cần lưu timezone
DECLARE @dto DATETIMEOFFSET = '2026-04-01 10:30:00 +07:00';
SELECT SWITCHOFFSET(@dto, '+00:00');  -- Convert sang UTC

-- Ví dụ thực tế
CREATE TABLE Orders (
    OrderId       INT PRIMARY KEY,
    OrderDate     DATE NOT NULL,          -- Chỉ cần ngày
    DeliveryTime  TIME(0),                -- Chỉ cần giờ giao
    CreatedAt     DATETIME2(7) NOT NULL DEFAULT SYSDATETIME(),  -- Timestamp đầy đủ
    UpdatedAt     DATETIME2(7),
    ScheduledAt   DATETIMEOFFSET          -- Nếu có múi giờ
);

-- So sánh datetime vs datetime2
SELECT 
    CAST('2026-04-01' AS DATETIME) AS old_datetime,
    CAST('2026-04-01' AS DATETIME2) AS new_datetime2;
-- datetime: '2026-04-01 00:00:00.000'
-- datetime2: '2026-04-01 00:00:00.0000000'

4. Kiểu nhị phân (Binary Types)

KiểuMô tảTối đa
binary(n)Fixed-length binary8,000 bytes
varbinary(n)Variable-length binary8,000 bytes
varbinary(MAX)Variable-length binary2 GB
imageDeprecated, thay bằng varbinary(MAX)2 GB
-- Dùng cho: hình ảnh, files, hashes, encrypted data
CREATE TABLE Documents (
    DocumentId    INT PRIMARY KEY,
    FileName      NVARCHAR(255) NOT NULL,
    FileContent   VARBINARY(MAX),                    -- File content
    ContentHash   BINARY(32),                        -- SHA-256 hash (32 bytes)
    Thumbnail     VARBINARY(MAX)
);

-- Lưu hash của password (KHÔNG lưu plain text!)
-- Trong thực tế nên hash ở application layer, không ở SQL

-- HASHBYTES để tạo hash
SELECT HASHBYTES('SHA2_256', N'password123');  -- Trả về varbinary(32)

-- Convert hex string sang binary và ngược lại
SELECT CONVERT(VARCHAR(64), HASHBYTES('SHA2_256', N'test'), 2);  -- Hex string

5. Kiểu đặc biệt

bit

-- bit: 0, 1, hoặc NULL. SQL Server tối ưu lưu 8 bit columns vào 1 byte
CREATE TABLE Settings (
    SettingId  INT PRIMARY KEY,
    IsActive   BIT NOT NULL DEFAULT 1,
    IsDeleted  BIT NOT NULL DEFAULT 0,
    IsPublic   BIT NOT NULL DEFAULT 0,
    HasExpiry  BIT NOT NULL DEFAULT 0
    -- 4 bit columns ở trên dùng chỉ 1 byte storage
);

-- Convert từ string
SELECT CAST('true' AS BIT);   -- Error! BIT không nhận 'true'
SELECT CAST(1 AS BIT);         -- 1
SELECT CAST(0 AS BIT);         -- 0
SELECT IIF(SomeCondition, CAST(1 AS BIT), CAST(0 AS BIT));

uniqueidentifier (GUID)

-- Lưu GUID (128-bit)
CREATE TABLE Sessions (
    SessionId   UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID(),
    UserId      INT NOT NULL,
    CreatedAt   DATETIME2 NOT NULL DEFAULT SYSDATETIME()
);

-- NEWID() - random GUID (gây index fragmentation vì random)
SELECT NEWID();  -- e.g., '6F9619FF-8B86-D011-B42D-00C04FC964FF'

-- NEWSEQUENTIALID() - sequential GUID (dùng làm PK clustered index tốt hơn)
-- Chỉ dùng được trong DEFAULT constraint
CREATE TABLE AuditLogs (
    LogId      UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWSEQUENTIALID(),
    Action     NVARCHAR(100),
    CreatedAt  DATETIME2 DEFAULT SYSDATETIME()
);

-- So sánh NEWID() vs NEWSEQUENTIALID()
-- NEWID(): Hoàn toàn random, gây page splits khi INSERT nhiều
-- NEWSEQUENTIALID(): Tăng dần, giảm fragmentation như IDENTITY

-- Convert GUID sang string và ngược lại
DECLARE @id UNIQUEIDENTIFIER = NEWID();
SELECT CAST(@id AS VARCHAR(36));  -- '6F961...'
SELECT CAST('6F9619FF-8B86-D011-B42D-00C04FC964FF' AS UNIQUEIDENTIFIER);

xml

-- Lưu XML data với validation tùy chọn
CREATE TABLE Configurations (
    ConfigId  INT PRIMARY KEY,
    ConfigKey NVARCHAR(100),
    ConfigXml XML  -- Untyped XML
);

-- Insert XML
INSERT INTO Configurations VALUES (1, 'AppSettings', 
    '<settings><timeout>30</timeout><maxRetry>3</maxRetry></settings>');

-- Query XML với XQuery
SELECT 
    ConfigId,
    ConfigXml.value('(/settings/timeout)[1]', 'INT') AS timeout,
    ConfigXml.value('(/settings/maxRetry)[1]', 'INT') AS max_retry
FROM Configurations
WHERE ConfigKey = 'AppSettings';

-- XML methods: value(), query(), nodes(), exist(), modify()
DECLARE @xml XML = '<employees><emp id="1">Alice</emp><emp id="2">Bob</emp></employees>';

SELECT 
    emp.value('@id', 'INT') AS emp_id,
    emp.value('.', 'NVARCHAR(100)') AS emp_name
FROM @xml.nodes('/employees/emp') AS t(emp);

JSON (via OPENJSON - SQL Server 2016+)

-- SQL Server không có native JSON type, dùng NVARCHAR + OPENJSON
-- Validate JSON
SELECT ISJSON('{"name": "Alice", "age": 30}');  -- 1 (valid)
SELECT ISJSON('not json');  -- 0

-- JSON_VALUE - lấy scalar value
DECLARE @json NVARCHAR(MAX) = '{"name": "Alice", "age": 30, "city": "Hanoi"}';
SELECT JSON_VALUE(@json, '$.name');  -- 'Alice'
SELECT JSON_VALUE(@json, '$.age');   -- '30'

-- JSON_QUERY - lấy object hoặc array
DECLARE @json2 NVARCHAR(MAX) = '{"person": {"name": "Alice", "skills": ["SQL", "C#"]}}';
SELECT JSON_QUERY(@json2, '$.person');         -- '{"name": "Alice", "skills": [...]}'
SELECT JSON_QUERY(@json2, '$.person.skills');  -- '["SQL", "C#"]'

-- OPENJSON - parse JSON thành rows
SELECT *
FROM OPENJSON('{"orders": [{"id":1,"total":100}, {"id":2,"total":200}]}', '$.orders')
WITH (
    order_id    INT           '$.id',
    order_total DECIMAL(10,2) '$.total'
);

-- FOR JSON - convert query result sang JSON
SELECT EmployeeId, FullName, Salary
FROM Employees
FOR JSON AUTO;

SELECT EmployeeId, FullName, Salary
FROM Employees
FOR JSON PATH, ROOT('employees');

sql_variant

-- Có thể lưu nhiều kiểu dữ liệu khác nhau (tránh dùng nếu có thể)
DECLARE @v SQL_VARIANT;
SET @v = 42;
SELECT @v, SQL_VARIANT_PROPERTY(@v, 'BaseType');  -- 'int'
SET @v = 'Hello';
SELECT @v, SQL_VARIANT_PROPERTY(@v, 'BaseType');  -- 'varchar'

rowversion / timestamp

-- Tự động tăng khi row được INSERT/UPDATE, dùng cho optimistic concurrency
CREATE TABLE Products (
    ProductId   INT PRIMARY KEY,
    ProductName NVARCHAR(200),
    Price       DECIMAL(10,2),
    RowVer      ROWVERSION NOT NULL  -- Tự cập nhật, 8 bytes
);

-- timestamp là synonym của rowversion (deprecated alias)
-- Dùng để detect concurrency conflicts
SELECT ProductId, ProductName, CAST(RowVer AS BIGINT) AS version
FROM Products;

6. Unicode vs Non-Unicode

-- N prefix tạo Unicode literal
SELECT 'Xin chào';        -- Mất dấu nếu collation không hỗ trợ
SELECT N'Xin chào';       -- Unicode, luôn đúng

-- Collation ảnh hưởng đến sorting và comparison
SELECT * FROM sys.fn_helpcollations() WHERE name LIKE 'Vietnamese%';

-- Khai báo explicit collation
CREATE TABLE MultiLanguage (
    NameEn NVARCHAR(200) COLLATE Latin1_General_CI_AS,
    NameVn NVARCHAR(200) COLLATE Vietnamese_CI_AS
);

-- So sánh cross-collation cần COLLATE trong query
SELECT * FROM t1
JOIN t2 ON t1.Name = t2.Name COLLATE SQL_Latin1_General_CP1_CI_AS;

7. Implicit vs Explicit Conversion

CAST vs CONVERT vs TRY_CAST vs TRY_CONVERT

-- CAST - chuẩn ANSI SQL
SELECT CAST(42 AS VARCHAR(10));         -- '42'
SELECT CAST('2026-04-01' AS DATE);      -- 2026-04-01
SELECT CAST(3.14 AS INT);              -- 3 (truncation)

-- CONVERT - SQL Server specific, hỗ trợ format code cho ngày
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(VARCHAR(8),  GETDATE(), 112);  -- '20260401' (yyyymmdd)
SELECT CONVERT(INT, '42');

-- TRY_CAST - trả về NULL thay vì error nếu convert thất bại
SELECT TRY_CAST('abc' AS INT);          -- NULL (không báo lỗi)
SELECT TRY_CAST('42' AS INT);           -- 42

-- TRY_CONVERT - tương tự TRY_CAST với format code
SELECT TRY_CONVERT(DATE, '2026-13-01'); -- NULL (tháng 13 không tồn tại)
SELECT TRY_CONVERT(DATE, '2026-04-01'); -- 2026-04-01

-- Implicit conversion - SQL Server tự convert (CÓ THỂ GÂY VẤN ĐỀ!)
SELECT * FROM Employees WHERE EmployeeId = '42';
-- SQL Server convert '42' sang INT: OK nhưng có thể ảnh hưởng performance

-- Implicit conversion GÂY VẤN ĐỀ với indexes:
-- Nếu column là VARCHAR và bạn so sánh với INT, SQL Server phải convert TỪNG GIÁ TRỊ TRONG TABLE
-- -> Index không được dùng -> Table scan!
SELECT * FROM Employees WHERE VarcharId = 42;  -- BAD: convert mỗi row
SELECT * FROM Employees WHERE VarcharId = '42'; -- GOOD: dùng index

-- Kiểm tra implicit conversion với execution plan:
-- Tìm "CONVERT_IMPLICIT" trong execution plan để phát hiện vấn đề

Bảng Conversion

From \ ToINTVARCHARDATEFLOATBIT
INT-✅ Explicit✅ Implicit✅ Implicit
VARCHAR✅ Explicit-✅ Explicit✅ Explicit
DATE✅ Explicit-
FLOAT✅ Explicit (truncate)✅ Explicit-✅ Implicit

8. Data Type Best Practices

-- 1. Chọn kiểu nhỏ nhất phù hợp
-- BAD: dùng BIGINT cho tất cả
CREATE TABLE BadDesign (OrderId BIGINT, Status BIGINT, YearsExp BIGINT);

-- GOOD: chọn đúng kích thước
CREATE TABLE GoodDesign (
    OrderId   INT      NOT NULL,  -- Đủ cho hầu hết use cases
    Status    TINYINT  NOT NULL,  -- 0-255 là đủ cho status
    YearsExp  TINYINT  NOT NULL   -- 0-120 năm kinh nghiệm
);

-- 2. Không dùng float/real cho tiền tệ
-- BAD:
CREATE TABLE BadMoney (Price FLOAT);  -- Sai về độ chính xác!

-- GOOD:
CREATE TABLE GoodMoney (Price DECIMAL(19, 4));  -- Chính xác

-- 3. Luôn dùng nvarchar cho user-input text trong ứng dụng đa ngôn ngữ
CREATE TABLE UserProfiles (
    UserId    INT           PRIMARY KEY,
    UserName  VARCHAR(50)   NOT NULL,       -- Username: ASCII-safe
    FullName  NVARCHAR(200) NOT NULL,       -- Tên: cần Unicode
    Email     VARCHAR(320)  NOT NULL,       -- Email: ASCII-safe
    Bio       NVARCHAR(MAX)                 -- Bio: user input
);

-- 4. Dùng DATE thay DATETIME khi không cần time component
-- BAD: lưu ngày sinh với DATETIME
CREATE TABLE BadDates (BirthDate DATETIME);  -- Lưu '1990-05-15 00:00:00.000'

-- GOOD:
CREATE TABLE GoodDates (BirthDate DATE);  -- Chỉ '1990-05-15', tiết kiệm 5 bytes/row

-- 5. Dùng DATETIME2 thay DATETIME cho timestamps mới
-- datetime: range 1753+, precision 3.33ms, 8 bytes
-- datetime2: range 0001+, precision 100ns, 6-8 bytes
CREATE TABLE AuditLog (
    CreatedAt DATETIME2(7) NOT NULL DEFAULT SYSDATETIME()
);

-- 6. Tránh varchar(MAX) khi biết trước giới hạn
-- BAD: dùng MAX cho mọi thứ
CREATE TABLE BadVarchar (Name NVARCHAR(MAX), Code NVARCHAR(MAX));

-- GOOD: giới hạn phù hợp
CREATE TABLE GoodVarchar (
    Name NVARCHAR(200),    -- Tên người
    Code NVARCHAR(20),     -- Mã code ngắn
    Description NVARCHAR(2000),  -- Mô tả
    Content NVARCHAR(MAX)  -- Nội dung thực sự cần MAX
);

-- 7. NULL vs NOT NULL
-- Tránh allow NULL trừ khi thực sự cần thiết về mặt business logic
-- NULL gây phức tạp trong queries và có thể ảnh hưởng index efficiency

Bảng tổng hợp chọn kiểu dữ liệu

Tình huốngKhuyến nghị
ID / Primary Key (web app)INT hoặc BIGINT
ID / Primary Key (distributed)UNIQUEIDENTIFIER với NEWSEQUENTIALID()
Tiền tệDECIMAL(19, 4)
Phần trăm, tỉ lệDECIMAL(5, 4)
Tọa độ GPSFLOAT
Tên người, địa chỉNVARCHAR(n)
Email, usernameVARCHAR(n)
Mã code (fixed)CHAR(n)
Ngày sinh, ngày hợp đồngDATE
Timestamp tạo/sửaDATETIME2(7)
Thời gian với timezoneDATETIMEOFFSET
Flag on/offBIT
Trạng thái nhỏ (< 255 options)TINYINT
File, image, binary dataVARBINARY(MAX)
Config JSON/XMLNVARCHAR(MAX) (JSON) hoặc XML
Audit/concurrent controlROWVERSION

DDL - Data Definition Language

DDL (Data Definition Language) là tập hợp các câu lệnh SQL dùng để tạo, thay đổi, và xóa cấu trúc database objects (database, table, index, schema, v.v.). Các câu lệnh DDL chính: CREATE, ALTER, DROP, TRUNCATE.


1. Database Objects

CREATE DATABASE

-- Tạo database đơn giản
CREATE DATABASE SalesDB;

-- Tạo database với cấu hình chi tiết
CREATE DATABASE SalesDB
ON PRIMARY (
    NAME = 'SalesDB',                          -- Logical name
    FILENAME = 'D:\Data\SalesDB.mdf',          -- Physical file path
    SIZE = 1024 MB,                            -- Initial size
    MAXSIZE = 10240 MB,                        -- Maximum size
    FILEGROWTH = 512 MB                        -- Auto-growth increment
)
LOG ON (
    NAME = 'SalesDB_log',
    FILENAME = 'D:\Logs\SalesDB_log.ldf',
    SIZE = 256 MB,
    MAXSIZE = 4096 MB,
    FILEGROWTH = 128 MB
);

-- Sử dụng database
USE SalesDB;
GO

-- Xem danh sách databases
SELECT name, state_desc, recovery_model_desc, compatibility_level
FROM sys.databases
ORDER BY name;

ALTER DATABASE

-- Thay đổi recovery model
ALTER DATABASE SalesDB SET RECOVERY SIMPLE;
ALTER DATABASE SalesDB SET RECOVERY FULL;
ALTER DATABASE SalesDB SET RECOVERY BULK_LOGGED;

-- Thay đổi compatibility level
ALTER DATABASE SalesDB SET COMPATIBILITY_LEVEL = 160;  -- SQL Server 2022

-- Thêm data file vào filegroup
ALTER DATABASE SalesDB
ADD FILE (
    NAME = 'SalesDB_data2',
    FILENAME = 'D:\Data\SalesDB_data2.ndf',
    SIZE = 512 MB,
    FILEGROWTH = 256 MB
) TO FILEGROUP [PRIMARY];

-- Đặt database thành READ-ONLY
ALTER DATABASE SalesDB SET READ_ONLY;
ALTER DATABASE SalesDB SET READ_WRITE;

-- Đổi tên database
ALTER DATABASE SalesDB MODIFY NAME = SalesDatabase;

DROP DATABASE

-- Xóa database (KHÔNG THỂ HOÀN TÁC!)
DROP DATABASE SalesDB;

-- An toàn hơn: kiểm tra trước khi xóa
IF EXISTS (SELECT 1 FROM sys.databases WHERE name = 'SalesDB')
    DROP DATABASE SalesDB;

-- SQL Server 2016+
DROP DATABASE IF EXISTS SalesDB;

2. Tables (Bảng dữ liệu)

CREATE TABLE - Cơ bản

-- Cú pháp đầy đủ
CREATE TABLE Employees (
    -- Integer types
    EmployeeId    INT             NOT NULL,
    DepartmentId  INT             NULL,
    
    -- String types  
    FirstName     NVARCHAR(100)   NOT NULL,
    LastName      NVARCHAR(100)   NOT NULL,
    Email         VARCHAR(320)    NOT NULL,
    Phone         VARCHAR(20)     NULL,
    
    -- Numeric
    Salary        DECIMAL(15, 2)  NOT NULL DEFAULT 0,
    
    -- Date types
    BirthDate     DATE            NULL,
    HireDate      DATE            NOT NULL DEFAULT CAST(GETDATE() AS DATE),
    CreatedAt     DATETIME2(7)    NOT NULL DEFAULT SYSDATETIME(),
    UpdatedAt     DATETIME2(7)    NULL,
    
    -- Boolean
    IsActive      BIT             NOT NULL DEFAULT 1,
    
    -- Constraints (inline)
    CONSTRAINT PK_Employees PRIMARY KEY (EmployeeId),
    CONSTRAINT FK_Employees_Departments FOREIGN KEY (DepartmentId)
        REFERENCES Departments(DepartmentId),
    CONSTRAINT UQ_Employees_Email UNIQUE (Email),
    CONSTRAINT CHK_Employees_Salary CHECK (Salary >= 0),
    CONSTRAINT CHK_Employees_BirthDate CHECK (BirthDate IS NULL OR BirthDate < CAST(GETDATE() AS DATE))
);

Các loại Constraints

PRIMARY KEY

-- Column-level (single column)
CREATE TABLE Orders (
    OrderId INT NOT NULL CONSTRAINT PK_Orders PRIMARY KEY,
    ...
);

-- Hoặc viết ngắn gọn
CREATE TABLE Orders (
    OrderId INT PRIMARY KEY,
    ...
);

-- Table-level (composite key)
CREATE TABLE OrderDetails (
    OrderId    INT NOT NULL,
    ProductId  INT NOT NULL,
    Quantity   INT NOT NULL,
    UnitPrice  DECIMAL(10,2) NOT NULL,
    
    CONSTRAINT PK_OrderDetails PRIMARY KEY (OrderId, ProductId)
    -- Composite PK phải dùng table-level constraint
);

FOREIGN KEY

CREATE TABLE Orders (
    OrderId      INT  PRIMARY KEY,
    CustomerId   INT  NOT NULL,
    EmployeeId   INT  NULL,
    
    -- Column-level FK
    CustomerId INT REFERENCES Customers(CustomerId),
    
    -- Table-level FK với đặt tên rõ ràng (khuyến nghị)
    CONSTRAINT FK_Orders_Customers FOREIGN KEY (CustomerId)
        REFERENCES Customers(CustomerId)
        ON DELETE NO ACTION    -- Không xóa cascading
        ON UPDATE CASCADE,     -- Tự cập nhật khi Customer PK thay đổi
    
    CONSTRAINT FK_Orders_Employees FOREIGN KEY (EmployeeId)
        REFERENCES Employees(EmployeeId)
        ON DELETE SET NULL     -- Đặt NULL khi Employee bị xóa
        ON UPDATE CASCADE
);

UNIQUE Constraint

CREATE TABLE Users (
    UserId    INT  PRIMARY KEY,
    Username  NVARCHAR(50) NOT NULL,
    Email     VARCHAR(320) NOT NULL,
    SSN       CHAR(9) NULL,
    
    -- Single column unique
    CONSTRAINT UQ_Users_Username UNIQUE (Username),
    CONSTRAINT UQ_Users_Email UNIQUE (Email),
    
    -- Composite unique (NULL values được phép - nhiều NULLs không vi phạm UNIQUE)
    CONSTRAINT UQ_Users_SSN UNIQUE (SSN)
);

-- Filtered unique index (SQL Server) - unique chỉ với non-NULL values
CREATE UNIQUE INDEX UX_Users_SSN_NotNull 
ON Users (SSN) 
WHERE SSN IS NOT NULL;

CHECK Constraint

CREATE TABLE Products (
    ProductId   INT PRIMARY KEY,
    ProductName NVARCHAR(200) NOT NULL,
    Price       DECIMAL(10,2) NOT NULL,
    Stock       INT           NOT NULL,
    Category    NVARCHAR(50)  NOT NULL,
    Status      CHAR(1)       NOT NULL DEFAULT 'A',
    
    CONSTRAINT CHK_Products_Price   CHECK (Price > 0),
    CONSTRAINT CHK_Products_Stock   CHECK (Stock >= 0),
    CONSTRAINT CHK_Products_Status  CHECK (Status IN ('A', 'I', 'D')),  -- Active, Inactive, Discontinued
    CONSTRAINT CHK_Products_Name    CHECK (LEN(ProductName) >= 2)
);

DEFAULT Constraint

CREATE TABLE AuditLog (
    LogId       INT        PRIMARY KEY,
    TableName   NVARCHAR(128) NOT NULL,
    Action      CHAR(1)       NOT NULL,
    
    -- Default values
    CreatedAt   DATETIME2(7)  NOT NULL CONSTRAINT DF_AuditLog_CreatedAt DEFAULT SYSDATETIME(),
    CreatedBy   NVARCHAR(128) NOT NULL CONSTRAINT DF_AuditLog_CreatedBy DEFAULT SUSER_SNAME(),
    IsProcessed BIT           NOT NULL CONSTRAINT DF_AuditLog_IsProcessed DEFAULT 0,
    
    CONSTRAINT CHK_AuditLog_Action CHECK (Action IN ('I', 'U', 'D'))
);

3. ALTER TABLE

-- Thêm column mới
ALTER TABLE Employees ADD MiddleName NVARCHAR(100) NULL;

-- Thêm column với DEFAULT (cho rows hiện có)
ALTER TABLE Employees ADD 
    CreatedBy NVARCHAR(128) NOT NULL DEFAULT SUSER_SNAME(),
    IsDeleted BIT NOT NULL DEFAULT 0;

-- Thay đổi kiểu dữ liệu của column
ALTER TABLE Employees ALTER COLUMN Phone VARCHAR(30) NULL;
-- Chú ý: không thể ALTER column nếu có INDEX hoặc CONSTRAINT trên column đó

-- Xóa column
ALTER TABLE Employees DROP COLUMN MiddleName;

-- Thêm constraint
ALTER TABLE Employees 
    ADD CONSTRAINT CHK_Employees_Email CHECK (Email LIKE '%@%.%');

ALTER TABLE Employees 
    ADD CONSTRAINT FK_Employees_Departments 
    FOREIGN KEY (DepartmentId) REFERENCES Departments(DepartmentId);

-- Xóa constraint
ALTER TABLE Employees DROP CONSTRAINT CHK_Employees_Email;
ALTER TABLE Employees DROP CONSTRAINT FK_Employees_Departments;

-- Xóa default constraint
ALTER TABLE Employees DROP CONSTRAINT DF_Employees_CreatedAt;

-- Disable / Enable constraint tạm thời
ALTER TABLE Employees NOCHECK CONSTRAINT FK_Employees_Departments;
ALTER TABLE Employees CHECK CONSTRAINT FK_Employees_Departments;

-- Xóa bảng (KHÔNG THỂ HOÀN TÁC)
DROP TABLE IF EXISTS Employees;
DROP TABLE Employees;  -- Lỗi nếu table không tồn tại

4. Schemas (Lược đồ)

Schema là namespace logic để nhóm các database objects:

-- Tạo schema
CREATE SCHEMA Hr AUTHORIZATION dbo;
CREATE SCHEMA Sales;
CREATE SCHEMA Finance;

-- Tạo table trong schema
CREATE TABLE Hr.Employees (...);
CREATE TABLE Sales.Orders (...);
CREATE TABLE Finance.Invoices (...);

-- Move table sang schema khác
ALTER SCHEMA Sales TRANSFER dbo.OldOrdersTable;

-- Xem các schemas
SELECT schema_id, name, principal_id
FROM sys.schemas
ORDER BY name;

-- Xem objects trong schema
SELECT 
    s.name AS schema_name,
    o.name AS object_name,
    o.type_desc
FROM sys.objects o
JOIN sys.schemas s ON o.schema_id = s.schema_id
WHERE o.type IN ('U', 'V', 'P')  -- Tables, Views, Stored Procedures
ORDER BY s.name, o.name;

-- Drop schema (phải empty trước)
DROP SCHEMA Finance;

Best Practice: Dùng schemas để:

  • Phân tách theo domain: Hr, Sales, Finance
  • Phân quyền theo nhóm: GRANT trên schema thay vì từng table
  • Tránh naming conflicts

5. Identity Columns và Sequences

IDENTITY

-- IDENTITY(seed, increment)
CREATE TABLE Products (
    ProductId   INT IDENTITY(1, 1) PRIMARY KEY,  -- Bắt đầu từ 1, tăng 1
    ProductName NVARCHAR(200) NOT NULL
);

-- INSERT không cần specify ProductId
INSERT INTO Products (ProductName) VALUES ('Widget A');
INSERT INTO Products (ProductName) VALUES ('Widget B');

-- Lấy giá trị IDENTITY vừa insert
SELECT SCOPE_IDENTITY();   -- Khuyến nghị: chỉ trong scope hiện tại
SELECT @@IDENTITY;          -- Trả về identity của lần INSERT cuối trong session (kể cả trigger)
SELECT IDENT_CURRENT('Products');  -- Identity hiện tại của table

-- INSERT với IDENTITY_INSERT ON (khi cần insert giá trị cụ thể)
SET IDENTITY_INSERT Products ON;
INSERT INTO Products (ProductId, ProductName) VALUES (100, 'Special Item');
SET IDENTITY_INSERT Products OFF;

-- Reset IDENTITY
DBCC CHECKIDENT ('Products', RESEED, 0);  -- Reset về 0, next insert = 1

-- Xem thông tin IDENTITY
SELECT 
    name AS column_name,
    seed_value,
    increment_value,
    last_value
FROM sys.identity_columns
WHERE object_id = OBJECT_ID('Products');

NEWID() và NEWSEQUENTIALID()

-- NEWID(): Random GUID - BAD cho clustered PK vì gây fragmentation
CREATE TABLE Sessions_Bad (
    SessionId UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(),  -- Random, gây page splits
    UserId    INT
);

-- NEWSEQUENTIALID(): Sequential GUID - tốt hơn cho clustered PK
CREATE TABLE Sessions_Good (
    SessionId UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWSEQUENTIALID(),  -- Sequential
    UserId    INT
);
-- Lưu ý: NEWSEQUENTIALID() chỉ dùng được trong DEFAULT constraint
-- Không gọi trực tiếp trong query được

-- Tại sao NEWID() gây vấn đề?
-- Mỗi INSERT có thể xen vào giữa các pages hiện có
-- -> Page splits -> Fragmentation -> Hiệu năng giảm

CREATE SEQUENCE

-- SEQUENCE là đối tượng độc lập, linh hoạt hơn IDENTITY
CREATE SEQUENCE dbo.OrderIdSeq
    START WITH 1000
    INCREMENT BY 1
    MINVALUE 1000
    MAXVALUE 9999999
    NO CYCLE                    -- Không quay lại từ đầu khi đến max
    CACHE 50;                   -- Cache 50 giá trị để tăng hiệu năng

-- Sử dụng SEQUENCE
INSERT INTO Orders (OrderId, CustomerId)
VALUES (NEXT VALUE FOR dbo.OrderIdSeq, 42);

-- Dùng trong DEFAULT
CREATE TABLE Orders (
    OrderId    INT DEFAULT (NEXT VALUE FOR dbo.OrderIdSeq),
    CustomerId INT,
    OrderDate  DATE DEFAULT GETDATE()
);

-- Xem giá trị hiện tại
SELECT current_value FROM sys.sequences WHERE name = 'OrderIdSeq';

-- Đặt lại SEQUENCE
ALTER SEQUENCE dbo.OrderIdSeq RESTART WITH 1000;

-- Ưu điểm SEQUENCE vs IDENTITY:
-- - Có thể dùng cho nhiều tables
-- - Có thể lấy giá trị trước khi INSERT (cho audit, logging)
-- - Hỗ trợ cycle
-- - Linh hoạt về min/max/increment

DECLARE @nextId INT = NEXT VALUE FOR dbo.OrderIdSeq;
-- Bây giờ có thể dùng @nextId trước khi INSERT

6. Computed Columns

-- Non-persisted computed column (tính lại mỗi khi query)
CREATE TABLE Rectangles (
    RectId  INT PRIMARY KEY,
    Width   DECIMAL(10,2) NOT NULL,
    Height  DECIMAL(10,2) NOT NULL,
    
    -- Tính toán mỗi lần query
    Area    AS (Width * Height),
    
    -- Persisted: lưu kết quả vào disk, tính lại khi INSERT/UPDATE
    Perimeter AS (2 * (Width + Height)) PERSISTED
);

-- Ví dụ thực tế
CREATE TABLE OrderDetails (
    OrderDetailId INT PRIMARY KEY,
    Quantity      INT            NOT NULL,
    UnitPrice     DECIMAL(10,2)  NOT NULL,
    Discount      DECIMAL(5,4)   NOT NULL DEFAULT 0,
    
    -- Non-persisted
    SubTotal AS (Quantity * UnitPrice),
    
    -- Persisted - có thể tạo index trên persisted computed column
    NetAmount AS (Quantity * UnitPrice * (1 - Discount)) PERSISTED
);

-- Index trên persisted computed column
CREATE INDEX IX_OrderDetails_NetAmount ON OrderDetails (NetAmount);

-- Xem computed columns
SELECT 
    name,
    definition,
    is_persisted
FROM sys.computed_columns
WHERE object_id = OBJECT_ID('OrderDetails');

7. Temporal Tables (System-Versioned)

Temporal tables tự động lưu lịch sử thay đổi dữ liệu:

-- Tạo temporal table
CREATE TABLE Employees (
    EmployeeId    INT             NOT NULL CONSTRAINT PK_Employees PRIMARY KEY,
    FullName      NVARCHAR(200)   NOT NULL,
    Salary        DECIMAL(15,2)   NOT NULL,
    DepartmentId  INT             NULL,
    
    -- Bắt buộc: 2 datetime2 columns cho period
    ValidFrom     DATETIME2(7)    GENERATED ALWAYS AS ROW START NOT NULL,
    ValidTo       DATETIME2(7)    GENERATED ALWAYS AS ROW END   NOT NULL,
    
    PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo)
)
WITH (SYSTEM_VERSIONING = ON (  
    HISTORY_TABLE = dbo.EmployeesHistory  -- SQL Server tự tạo history table
));

-- INSERT/UPDATE/DELETE hoạt động bình thường
UPDATE Employees SET Salary = 75000 WHERE EmployeeId = 1;
-- Row cũ tự động chuyển vào EmployeesHistory

-- Query dữ liệu lịch sử
-- Lấy dữ liệu tại một thời điểm cụ thể
SELECT * FROM Employees
FOR SYSTEM_TIME AS OF '2025-01-01 00:00:00';

-- Lấy tất cả versions trong khoảng thời gian
SELECT * FROM Employees
FOR SYSTEM_TIME BETWEEN '2025-01-01' AND '2026-01-01';

-- Lấy tất cả versions từng tồn tại trong khoảng
SELECT * FROM Employees
FOR SYSTEM_TIME FROM '2025-01-01' TO '2026-01-01';

-- Tất cả versions kể cả đã bị xóa
SELECT * FROM Employees
FOR SYSTEM_TIME ALL;

-- Xem history table
SELECT * FROM dbo.EmployeesHistory WHERE EmployeeId = 1 ORDER BY ValidFrom;

-- Tắt system versioning (để có thể DROP table)
ALTER TABLE Employees SET (SYSTEM_VERSIONING = OFF);
DROP TABLE dbo.EmployeesHistory;
DROP TABLE dbo.Employees;

8. Table Constraints vs Column Constraints

-- Column-level constraint (đặt ngay sau column definition)
CREATE TABLE Example (
    Col1 INT NOT NULL PRIMARY KEY,           -- Column constraint
    Col2 INT REFERENCES OtherTable(Id),      -- Inline FK
    Col3 INT CHECK (Col3 > 0),              -- Inline CHECK
    Col4 INT DEFAULT 42                      -- Inline DEFAULT
);

-- Table-level constraint (đặt sau tất cả columns)
CREATE TABLE Example (
    Col1 INT NOT NULL,
    Col2 INT,
    Col3 INT,
    Col4 INT,
    
    CONSTRAINT PK_Example PRIMARY KEY (Col1),             -- Named PK
    CONSTRAINT FK_Example_OtherTable FOREIGN KEY (Col2)   -- Named FK
        REFERENCES OtherTable(Id),
    CONSTRAINT CHK_Example_Col3 CHECK (Col3 > 0),         -- Named CHECK
    CONSTRAINT UQ_Example_Col3_Col4 UNIQUE (Col3, Col4)   -- Composite UNIQUE
);

-- PHẢI dùng table-level constraint khi:
-- 1. Composite PRIMARY KEY
-- 2. Composite UNIQUE
-- 3. Composite FOREIGN KEY (hiếm)
-- 4. Muốn đặt tên cho constraint (khuyến nghị)

-- Xem tất cả constraints của table
SELECT 
    cc.name AS constraint_name,
    cc.type_desc,
    cc.definition
FROM sys.check_constraints cc
WHERE OBJECT_NAME(cc.parent_object_id) = 'Example'

UNION ALL

SELECT 
    dc.name,
    'DEFAULT_CONSTRAINT',
    dc.definition
FROM sys.default_constraints dc
WHERE OBJECT_NAME(dc.parent_object_id) = 'Example';

9. Cascading Actions

-- ON DELETE / ON UPDATE options:
-- NO ACTION  - Mặc định. Báo lỗi nếu có FK references
-- CASCADE    - Tự xóa/cập nhật rows con
-- SET NULL   - Đặt FK = NULL khi parent bị xóa/cập nhật
-- SET DEFAULT - Đặt FK = DEFAULT value khi parent bị xóa/cập nhật
-- RESTRICT   - Tương tự NO ACTION (ANSI standard, không dùng trong SQL Server)

CREATE TABLE Departments (
    DepartmentId INT PRIMARY KEY,
    Name         NVARCHAR(100) NOT NULL
);

CREATE TABLE Employees (
    EmployeeId    INT PRIMARY KEY,
    DepartmentId  INT NOT NULL,
    ManagerId     INT NULL,
    
    -- CASCADE: xóa Dept -> xóa tất cả employees của dept đó
    CONSTRAINT FK_Employees_Departments FOREIGN KEY (DepartmentId)
        REFERENCES Departments(DepartmentId)
        ON DELETE CASCADE
        ON UPDATE CASCADE,
    
    -- SET NULL: xóa Manager -> ManagerId = NULL
    CONSTRAINT FK_Employees_Manager FOREIGN KEY (ManagerId)
        REFERENCES Employees(EmployeeId)
        ON DELETE SET NULL
        ON UPDATE NO ACTION
);

-- Lưu ý: SQL Server KHÔNG cho phép circular cascade
-- Và không cho phép multiple cascade paths đến cùng 1 table
-- Ví dụ: Orders -> Customers (ON DELETE CASCADE) 
--        Orders -> Employees (ON DELETE CASCADE)
--        Nếu thêm Customers -> Employees (ON DELETE CASCADE) -> ERROR: multiple cascade paths

-- Ví dụ ON DELETE SET DEFAULT
CREATE TABLE Tickets (
    TicketId    INT PRIMARY KEY,
    AssignedTo  INT NOT NULL DEFAULT 0,  -- 0 = Unassigned
    
    CONSTRAINT FK_Tickets_Users FOREIGN KEY (AssignedTo)
        REFERENCES Users(UserId)
        ON DELETE SET DEFAULT  -- Khi User bị xóa, ticket về trạng thái unassigned
        ON UPDATE CASCADE
);

10. Indexes trong DDL

-- CLUSTERED INDEX: Sắp xếp vật lý dữ liệu (mỗi table chỉ có 1)
-- PRIMARY KEY mặc định là clustered
CREATE TABLE Orders (
    OrderId INT NOT NULL,
    OrderDate DATE NOT NULL,
    CustomerId INT,
    
    CONSTRAINT PK_Orders PRIMARY KEY CLUSTERED (OrderId)
);

-- Nếu muốn PK non-clustered và tạo clustered index trên column khác
CREATE TABLE Orders (
    OrderId INT NOT NULL,
    OrderDate DATE NOT NULL,
    CustomerId INT,
    
    CONSTRAINT PK_Orders PRIMARY KEY NONCLUSTERED (OrderId)
);

CREATE CLUSTERED INDEX CIX_Orders_OrderDate ON Orders (OrderDate);
-- Clustered trên OrderDate -> data pages sắp xếp theo OrderDate

-- NONCLUSTERED INDEX (mặc định khi tạo CREATE INDEX)
CREATE NONCLUSTERED INDEX IX_Orders_CustomerId 
ON Orders (CustomerId)
INCLUDE (OrderDate, TotalAmount);  -- INCLUDE: đưa thêm columns vào leaf node

-- UNIQUE INDEX
CREATE UNIQUE INDEX UX_Employees_Email ON Employees (Email);
-- Khác với UNIQUE CONSTRAINT: index có thể hỗ trợ INCLUDE columns

-- FILTERED INDEX - index chỉ trên subset rows
CREATE NONCLUSTERED INDEX IX_Employees_ActiveSalary
ON Employees (Salary)
WHERE IsActive = 1;  -- Chỉ index active employees

-- COMPOSITE INDEX
CREATE INDEX IX_Orders_CustomerDate 
ON Orders (CustomerId, OrderDate DESC)
INCLUDE (TotalAmount, Status);

-- Xem indexes của table
SELECT 
    i.name AS index_name,
    i.type_desc,
    i.is_unique,
    STRING_AGG(c.name, ', ') WITHIN GROUP (ORDER BY ic.key_ordinal) AS key_columns
FROM sys.indexes i
JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
WHERE i.object_id = OBJECT_ID('Orders')
    AND ic.is_included_column = 0
GROUP BY i.name, i.type_desc, i.is_unique;

-- DROP INDEX
DROP INDEX IX_Orders_CustomerId ON Orders;

DML - Data Manipulation Language

DML (Data Manipulation Language) là tập hợp các câu lệnh SQL dùng để thêm, cập nhật, xóa, và đọc dữ liệu trong các tables. Các câu lệnh DML chính: INSERT, UPDATE, DELETE, MERGE, SELECT.


1. INSERT

INSERT cơ bản

-- Chỉ định tên columns (khuyến nghị - luôn làm vậy)
INSERT INTO Employees (FirstName, LastName, Email, DepartmentId, Salary, HireDate)
VALUES ('Nguyen', 'Van An', 'nvan.an@company.com', 3, 50000, '2026-01-15');

-- Không chỉ định columns (nguy hiểm - phụ thuộc vào column order)
INSERT INTO Employees
VALUES (NULL, 'Nguyen', 'Van An', 'nvan.an@company.com', NULL, 50000, '2026-01-15', 1);

-- INSERT và lấy lại ID vừa tạo
DECLARE @newId INT;
INSERT INTO Employees (FirstName, LastName, Email, DepartmentId, Salary)
VALUES ('Tran', 'Thi B', 'tthi.b@company.com', 2, 45000);
SET @newId = SCOPE_IDENTITY();
SELECT @newId;  -- Lấy IDENTITY value vừa insert

Multi-row INSERT

-- INSERT nhiều rows cùng lúc (SQL Server 2008+) - hiệu quả hơn nhiều lần INSERT
INSERT INTO Products (ProductName, Price, CategoryId, Stock)
VALUES 
    ('Widget A',  19.99, 1, 100),
    ('Widget B',  29.99, 1, 200),
    ('Gadget X',  49.99, 2, 50),
    ('Gadget Y',  79.99, 2, 75),
    ('Service Z', 99.99, 3, NULL);

-- Tối đa một VALUES clause có thể có 1000 rows

INSERT INTO…SELECT

-- Sao chép dữ liệu từ table khác
INSERT INTO EmployeesArchive (EmployeeId, FullName, Salary, ArchivedAt)
SELECT EmployeeId, FirstName + ' ' + LastName, Salary, SYSDATETIME()
FROM Employees
WHERE IsActive = 0 AND HireDate < '2020-01-01';

-- INSERT từ nhiều tables với JOIN
INSERT INTO OrderSummary (OrderId, CustomerName, TotalAmount, OrderDate)
SELECT 
    o.OrderId,
    c.FirstName + ' ' + c.LastName,
    SUM(od.Quantity * od.UnitPrice),
    o.OrderDate
FROM Orders o
JOIN Customers c ON o.CustomerId = c.CustomerId
JOIN OrderDetails od ON o.OrderId = od.OrderId
WHERE o.OrderDate >= '2026-01-01'
GROUP BY o.OrderId, c.FirstName, c.LastName, o.OrderDate;

-- INSERT với CTE
WITH NewHires AS (
    SELECT EmployeeId, FirstName, LastName, DepartmentId
    FROM Employees
    WHERE HireDate >= DATEADD(MONTH, -3, GETDATE())
)
INSERT INTO NewHireReport (EmployeeId, FullName, DeptName)
SELECT 
    nh.EmployeeId,
    nh.FirstName + ' ' + nh.LastName,
    d.DepartmentName
FROM NewHires nh
JOIN Departments d ON nh.DepartmentId = d.DepartmentId;

OUTPUT clause với INSERT

-- OUTPUT: capture rows bị ảnh hưởng
-- INSERTED table: rows được INSERT
-- DELETED table: rows bị DELETE

-- Lấy lại tất cả rows vừa INSERT
INSERT INTO Employees (FirstName, LastName, Email, Salary)
OUTPUT 
    INSERTED.EmployeeId,
    INSERTED.FirstName,
    INSERTED.LastName,
    INSERTED.CreatedAt
VALUES 
    ('Alice', 'Smith', 'alice@co.com', 60000),
    ('Bob',   'Jones', 'bob@co.com',   55000);

-- Lưu OUTPUT vào table khác
DECLARE @InsertedRows TABLE (
    EmployeeId INT,
    FullName   NVARCHAR(200),
    InsertedAt DATETIME2
);

INSERT INTO Employees (FirstName, LastName, Email, Salary)
OUTPUT 
    INSERTED.EmployeeId,
    INSERTED.FirstName + ' ' + INSERTED.LastName,
    INSERTED.CreatedAt
INTO @InsertedRows (EmployeeId, FullName, InsertedAt)
VALUES ('Charlie', 'Brown', 'charlie@co.com', 52000);

SELECT * FROM @InsertedRows;

2. UPDATE

UPDATE cơ bản

-- UPDATE với WHERE (LUÔN kiểm tra WHERE trước khi chạy!)
UPDATE Employees
SET Salary = Salary * 1.10,          -- Tăng 10%
    UpdatedAt = SYSDATETIME()
WHERE DepartmentId = 3
  AND IsActive = 1;

-- Update nhiều columns
UPDATE Products
SET 
    Price       = Price * 0.9,        -- Giảm 10%
    IsOnSale    = 1,
    SaleEndDate = DATEADD(DAY, 30, GETDATE()),
    UpdatedAt   = SYSDATETIME()
WHERE CategoryId = 5;

-- Update dùng subquery
UPDATE Orders
SET TotalAmount = (
    SELECT SUM(od.Quantity * od.UnitPrice)
    FROM OrderDetails od
    WHERE od.OrderId = Orders.OrderId
)
WHERE Status = 'Pending';

UPDATE với JOIN

-- UPDATE với JOIN (SQL Server specific syntax)
UPDATE e
SET 
    e.DepartmentName = d.DepartmentName,  -- Bất thường, nhưng ví dụ về UPDATE với JOIN
    e.ManagerId = d.ManagerId
FROM Employees e
INNER JOIN Departments d ON e.DepartmentId = d.DepartmentId
WHERE d.IsActive = 1;

-- UPDATE nhiều tables liên quan
UPDATE od
SET od.UnitPrice = p.CurrentPrice
FROM OrderDetails od
JOIN Products p ON od.ProductId = p.ProductId
JOIN Orders o ON od.OrderId = o.OrderId
WHERE o.Status = 'Draft'    -- Chỉ update orders chưa confirm
  AND od.UnitPrice <> p.CurrentPrice;

-- Ví dụ thực tế: tăng lương theo department target
UPDATE e
SET e.Salary = e.Salary * (1 + d.SalaryIncreaseRate)
FROM Employees e
JOIN Departments d ON e.DepartmentId = d.DepartmentId
WHERE e.IsActive = 1
  AND e.LastReviewDate < DATEADD(YEAR, -1, GETDATE())
  AND d.SalaryIncreaseRate > 0;

UPDATE với CTE

-- UPDATE qua CTE
WITH EmployeesToUpdate AS (
    SELECT 
        e.EmployeeId,
        e.Salary,
        e.Salary * 1.05 AS NewSalary,
        ROW_NUMBER() OVER (PARTITION BY e.DepartmentId ORDER BY e.Salary DESC) AS rn
    FROM Employees e
    WHERE e.IsActive = 1
)
UPDATE EmployeesToUpdate
SET Salary = NewSalary
WHERE rn <= 3;  -- Chỉ tăng lương top 3 người trong mỗi department

-- UPDATE với ranking
WITH RankedEmployees AS (
    SELECT 
        EmployeeId,
        Salary,
        RANK() OVER (ORDER BY PerformanceScore DESC) AS perf_rank
    FROM Employees
    WHERE ReviewYear = 2025
)
UPDATE Employees
SET Salary = Salary * CASE 
    WHEN re.perf_rank <= 10 THEN 1.15  -- Top 10: +15%
    WHEN re.perf_rank <= 30 THEN 1.10  -- Rank 11-30: +10%
    ELSE 1.05                          -- Others: +5%
END
FROM Employees e
JOIN RankedEmployees re ON e.EmployeeId = re.EmployeeId;

OUTPUT clause với UPDATE

-- Capture giá trị trước và sau khi UPDATE
DECLARE @SalaryChanges TABLE (
    EmployeeId  INT,
    OldSalary   DECIMAL(15,2),
    NewSalary   DECIMAL(15,2),
    ChangedAt   DATETIME2
);

UPDATE Employees
SET Salary = Salary * 1.10
OUTPUT 
    INSERTED.EmployeeId,
    DELETED.Salary,       -- Giá trị CŨ (trước khi update)
    INSERTED.Salary,      -- Giá trị MỚI (sau khi update)
    INSERTED.UpdatedAt
INTO @SalaryChanges (EmployeeId, OldSalary, NewSalary, ChangedAt)
WHERE DepartmentId = 3 AND IsActive = 1;

-- Xem kết quả thay đổi
SELECT 
    EmployeeId,
    OldSalary,
    NewSalary,
    NewSalary - OldSalary AS increase_amount,
    ChangedAt
FROM @SalaryChanges;

3. DELETE

DELETE cơ bản

-- DELETE với điều kiện (LUÔN có WHERE!)
DELETE FROM Employees WHERE EmployeeId = 42;

-- DELETE nhiều rows
DELETE FROM AuditLog
WHERE CreatedAt < DATEADD(YEAR, -2, GETDATE())
  AND IsProcessed = 1;

-- DELETE với subquery
DELETE FROM Orders
WHERE OrderId IN (
    SELECT o.OrderId
    FROM Orders o
    LEFT JOIN OrderDetails od ON o.OrderId = od.OrderId
    WHERE od.OrderId IS NULL  -- Orders không có items
      AND o.CreatedAt < DATEADD(DAY, -7, GETDATE())
);

DELETE với JOIN

-- DELETE với FROM...JOIN (SQL Server syntax)
DELETE od
FROM OrderDetails od
JOIN Orders o ON od.OrderId = o.OrderId
WHERE o.Status = 'Cancelled'
  AND o.CancelledAt < DATEADD(MONTH, -6, GETDATE());

-- Soft delete thường được ưa chuộng hơn hard delete
UPDATE Employees
SET 
    IsDeleted  = 1,
    DeletedAt  = SYSDATETIME(),
    DeletedBy  = SUSER_SNAME()
WHERE EmployeeId = 42;

OUTPUT clause với DELETE

-- Capture rows bị xóa trước khi xóa
DECLARE @DeletedOrders TABLE (
    OrderId     INT,
    CustomerId  INT,
    TotalAmount DECIMAL(10,2),
    DeletedAt   DATETIME2 DEFAULT SYSDATETIME()
);

DELETE FROM Orders
OUTPUT 
    DELETED.OrderId,
    DELETED.CustomerId,
    DELETED.TotalAmount,
    SYSDATETIME()
INTO @DeletedOrders (OrderId, CustomerId, TotalAmount, DeletedAt)
WHERE Status = 'Cancelled'
  AND OrderDate < DATEADD(YEAR, -3, GETDATE());

-- Lưu audit trail
INSERT INTO OrdersDeleteAudit
SELECT * FROM @DeletedOrders;

TRUNCATE TABLE vs DELETE

DELETETRUNCATE
WHERE clause✅ Có thể lọc❌ Không, xóa tất cả
Transaction logGhi từng row (slow)Chỉ ghi deallocations (fast)
TriggersKích hoạt DML triggersKhông kích hoạt
Identity resetGiữ nguyênReset về seed
Rollback✅ Có thể✅ Có thể (trong transaction)
Foreign KeysBị giới hạn bởi FKLỗi nếu có FK references
PermissionsDELETE permissionALTER TABLE permission
-- DELETE - chậm, log từng row, có thể ROLLBACK trong transaction
BEGIN TRANSACTION;
DELETE FROM TempData WHERE BatchId = 42;
ROLLBACK;  -- Khôi phục được

-- TRUNCATE - nhanh hơn, không kích hoạt triggers
TRUNCATE TABLE TempData;  -- Xóa tất cả, reset IDENTITY

-- TRUNCATE với Foreign Key: phải disable FK trước
ALTER TABLE ChildTable NOCHECK CONSTRAINT FK_Child_Parent;
TRUNCATE TABLE ParentTable;  -- Error nếu FK vẫn active

-- Delete theo batch để tránh lock cả table
DECLARE @batch_size INT = 10000;
DECLARE @deleted_count INT;

REPEAT_DELETE:
    DELETE TOP (@batch_size)
    FROM LargeAuditTable
    WHERE CreatedAt < DATEADD(YEAR, -5, GETDATE());
    
    SET @deleted_count = @@ROWCOUNT;
    
    WAITFOR DELAY '00:00:01';  -- Cho phép các transaction khác tiếp tục
    
IF @deleted_count = @batch_size GOTO REPEAT_DELETE;
-- Lặp đến khi không còn gì để xóa

4. MERGE Statement (Upsert)

MERGE kết hợp INSERT, UPDATE, DELETE trong một statement:

-- Cú pháp MERGE cơ bản
MERGE INTO TargetTable AS target
USING SourceTable AS source
ON target.Id = source.Id

WHEN MATCHED AND target.SomeColumn <> source.SomeColumn THEN
    UPDATE SET 
        target.SomeColumn = source.SomeColumn,
        target.UpdatedAt = SYSDATETIME()

WHEN NOT MATCHED BY TARGET THEN
    INSERT (Id, SomeColumn, CreatedAt)
    VALUES (source.Id, source.SomeColumn, SYSDATETIME())

WHEN NOT MATCHED BY SOURCE THEN
    DELETE;  -- Xóa rows trong target không có trong source

Ví dụ thực tế - Product sync

-- Đồng bộ sản phẩm từ staging table vào production
MERGE INTO Products AS target
USING StagingProducts AS source
ON target.ProductSku = source.ProductSku

WHEN MATCHED AND (
    target.ProductName <> source.ProductName OR
    target.Price <> source.Price OR
    target.Stock <> source.Stock
) THEN
    UPDATE SET
        target.ProductName = source.ProductName,
        target.Price       = source.Price,
        target.Stock       = source.Stock,
        target.UpdatedAt   = SYSDATETIME()

WHEN NOT MATCHED BY TARGET THEN
    INSERT (ProductSku, ProductName, Price, Stock, CreatedAt)
    VALUES (source.ProductSku, source.ProductName, source.Price, source.Stock, SYSDATETIME())

WHEN NOT MATCHED BY SOURCE AND target.IsActive = 1 THEN
    UPDATE SET target.IsActive = 0, target.UpdatedAt = SYSDATETIME()

OUTPUT
    $action AS merge_action,          -- 'INSERT', 'UPDATE', 'DELETE'
    INSERTED.ProductSku,
    DELETED.Price AS old_price,
    INSERTED.Price AS new_price;

Upsert Pattern (Simple)

-- Đơn giản hơn MERGE khi chỉ cần INSERT hoặc UPDATE

-- Option 1: MERGE (an toàn nhất)
MERGE INTO UserPreferences AS target
USING (VALUES (@userId, @key, @value)) AS source(UserId, PrefKey, PrefValue)
ON target.UserId = source.UserId AND target.PrefKey = source.PrefKey
WHEN MATCHED THEN
    UPDATE SET target.PrefValue = source.PrefValue, target.UpdatedAt = SYSDATETIME()
WHEN NOT MATCHED THEN
    INSERT (UserId, PrefKey, PrefValue, CreatedAt)
    VALUES (source.UserId, source.PrefKey, source.PrefValue, SYSDATETIME());

-- Option 2: IF EXISTS (đơn giản nhưng có race condition)
IF EXISTS (SELECT 1 FROM UserPreferences WHERE UserId = @userId AND PrefKey = @key)
    UPDATE UserPreferences 
    SET PrefValue = @value, UpdatedAt = SYSDATETIME()
    WHERE UserId = @userId AND PrefKey = @key;
ELSE
    INSERT INTO UserPreferences (UserId, PrefKey, PrefValue)
    VALUES (@userId, @key, @value);

5. SELECT INTO

-- Tạo table mới từ kết quả query (không cần CREATE TABLE trước)
SELECT EmployeeId, FirstName + ' ' + LastName AS FullName, Salary, DepartmentId
INTO EmployeesBackup              -- Tạo table mới
FROM Employees
WHERE IsActive = 1;

-- SELECT INTO tạo table với cùng cấu trúc nhưng KHÔNG copy:
-- - Constraints (PK, FK, UNIQUE, CHECK)
-- - Indexes
-- - Triggers

-- Tạo empty table với cùng cấu trúc
SELECT * INTO EmptyEmployees
FROM Employees
WHERE 1 = 0;  -- Không có rows nào thỏa

-- Tạo table trong database khác
SELECT * INTO OtherDB.dbo.EmployeesBackup
FROM Employees;

-- SELECT INTO với tempdb (temporary table)
SELECT EmployeeId, Salary
INTO #TempSalaries  -- Temp table (# prefix)
FROM Employees;

SELECT EmployeeId, AVG(Salary) OVER (PARTITION BY DepartmentId) AS dept_avg
INTO ##GlobalTemp    -- Global temp table (## prefix)
FROM Employees;

6. Bulk Operations

BULK INSERT

-- Import dữ liệu từ file CSV
BULK INSERT Products
FROM 'D:\Data\products.csv'
WITH (
    FORMAT = 'CSV',              -- SQL Server 2017+
    FIELDTERMINATOR = ',',       -- Phân cách cột
    ROWTERMINATOR = '\n',        -- Phân cách dòng
    FIRSTROW = 2,                -- Bỏ qua header row
    MAXERRORS = 10,              -- Cho phép tối đa 10 lỗi
    ERRORFILE = 'D:\Errors\products_errors.txt',
    TABLOCK                      -- Table-level lock, nhanh hơn
);

-- Import với format file
BULK INSERT Products
FROM 'D:\Data\products.dat'
WITH (
    FORMATFILE = 'D:\Data\products.fmt',  -- File mô tả format
    BATCHSIZE = 5000,            -- Commit mỗi 5000 rows
    TABLOCK
);

OPENROWSET

-- Import từ file ad-hoc
INSERT INTO Products (ProductName, Price, CategoryId)
SELECT ProductName, Price, CategoryId
FROM OPENROWSET(
    BULK 'D:\Data\products.csv',
    FORMATFILE = 'D:\Data\products.fmt'
) AS bulk_data;

-- Import từ Excel (cần OLEDB provider)
SELECT * 
FROM OPENROWSET(
    'Microsoft.ACE.OLEDB.12.0',
    'Excel 12.0;Database=D:\Data\products.xlsx;HDR=YES',
    'SELECT * FROM [Sheet1$]'
);

Table-Valued Parameters (TVP)

-- Tạo user-defined table type
CREATE TYPE dbo.ProductList AS TABLE (
    ProductId   INT NOT NULL,
    ProductName NVARCHAR(200) NOT NULL,
    Price       DECIMAL(10,2) NOT NULL,
    
    PRIMARY KEY (ProductId)
);

-- Stored procedure nhận TVP
CREATE PROCEDURE dbo.BulkInsertProducts
    @Products dbo.ProductList READONLY  -- READONLY là bắt buộc cho TVP parameters
AS
BEGIN
    INSERT INTO Products (ProductId, ProductName, Price)
    SELECT ProductId, ProductName, Price
    FROM @Products;
END;

-- Sử dụng từ T-SQL
DECLARE @myProducts dbo.ProductList;
INSERT INTO @myProducts (ProductId, ProductName, Price)
VALUES (1, 'Widget A', 19.99), (2, 'Widget B', 29.99);

EXEC dbo.BulkInsertProducts @Products = @myProducts;

7. Transactions với DML

-- Transaction đảm bảo ACID properties
BEGIN TRANSACTION;

    -- Tất cả hoặc không có gì
    INSERT INTO Orders (CustomerId, OrderDate, TotalAmount)
    VALUES (42, GETDATE(), 299.99);

    DECLARE @orderId INT = SCOPE_IDENTITY();

    INSERT INTO OrderDetails (OrderId, ProductId, Quantity, UnitPrice)
    VALUES (@orderId, 101, 2, 99.99),
           (@orderId, 202, 1, 100.01);

    -- Cập nhật stock
    UPDATE Products
    SET Stock = Stock - 2
    WHERE ProductId = 101;
    
    UPDATE Products
    SET Stock = Stock - 1
    WHERE ProductId = 202;

    -- Kiểm tra stock không âm
    IF EXISTS (SELECT 1 FROM Products WHERE ProductId IN (101, 202) AND Stock < 0)
    BEGIN
        ROLLBACK TRANSACTION;
        RAISERROR('Insufficient stock', 16, 1);
        RETURN;
    END

COMMIT TRANSACTION;

-- Transaction với error handling
BEGIN TRY
    BEGIN TRANSACTION;

        UPDATE Accounts SET Balance = Balance - 500 WHERE AccountId = 1;
        UPDATE Accounts SET Balance = Balance + 500 WHERE AccountId = 2;
        
        -- Kiểm tra balance
        IF EXISTS (SELECT 1 FROM Accounts WHERE AccountId = 1 AND Balance < 0)
            THROW 50001, 'Insufficient funds.', 1;

    COMMIT TRANSACTION;
    PRINT 'Transfer successful';
END TRY
BEGIN CATCH
    IF @@TRANCOUNT > 0
        ROLLBACK TRANSACTION;
    
    DECLARE @ErrorMessage NVARCHAR(4000) = ERROR_MESSAGE();
    DECLARE @ErrorSeverity INT = ERROR_SEVERITY();
    DECLARE @ErrorState INT = ERROR_STATE();
    
    RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState);
END CATCH;

8. Performance Considerations

Set-based vs Row-by-row Operations

-- BAD: Cursor (row-by-row) - rất chậm cho large datasets
DECLARE @EmployeeId INT, @Salary DECIMAL(15,2);

DECLARE emp_cursor CURSOR FOR
    SELECT EmployeeId, Salary FROM Employees WHERE IsActive = 1;

OPEN emp_cursor;
FETCH NEXT FROM emp_cursor INTO @EmployeeId, @Salary;

WHILE @@FETCH_STATUS = 0
BEGIN
    UPDATE Employees
    SET Salary = @Salary * 1.05
    WHERE EmployeeId = @EmployeeId;
    
    FETCH NEXT FROM emp_cursor INTO @EmployeeId, @Salary;
END;

CLOSE emp_cursor;
DEALLOCATE emp_cursor;

-- GOOD: Set-based (một lần cho tất cả)
UPDATE Employees
SET Salary = Salary * 1.05
WHERE IsActive = 1;

-- Set-based thường nhanh hơn cursor 10-100x cho large datasets!

Batch Updates cho Large Tables

-- Chia UPDATE/DELETE thành batches để giảm lock pressure
DECLARE @batch_size INT = 5000;
DECLARE @rows_affected INT = 1;

WHILE @rows_affected > 0
BEGIN
    UPDATE TOP (@batch_size) e
    SET e.IsArchived = 1, e.ArchivedAt = SYSDATETIME()
    FROM Employees e
    WHERE e.IsActive = 0 
      AND e.TerminationDate < DATEADD(YEAR, -3, GETDATE())
      AND e.IsArchived = 0;
    
    SET @rows_affected = @@ROWCOUNT;
    
    -- Nhường CPU và lock cho các processes khác
    WAITFOR DELAY '00:00:00.500';  -- 500ms pause
END;

Avoiding Common DML Pitfalls

-- 1. Luôn có WHERE trong UPDATE/DELETE (kiểm tra kỹ!)
-- Test với SELECT trước
SELECT * FROM Employees WHERE DepartmentId = 99;  -- Xem có đúng không
UPDATE Employees SET Salary = 0 WHERE DepartmentId = 99;  -- Sau đó UPDATE

-- 2. Dùng OUTPUT để audit changes quan trọng
-- 3. Test trong transaction, rollback nếu số rows không đúng
BEGIN TRANSACTION;
UPDATE Products SET Price = Price * 0.9 WHERE CategoryId = 5;
SELECT @@ROWCOUNT AS rows_updated;  -- Kiểm tra số rows
-- Nếu đúng thì: COMMIT TRANSACTION;
-- Nếu sai thì: ROLLBACK TRANSACTION;
ROLLBACK TRANSACTION;  -- Nhớ commit hoặc rollback!

-- 4. MERGE có thể có edge cases - test kỹ
-- 5. Avoid implicit conversion trong WHERE clause của DML
-- BAD:
DELETE FROM Orders WHERE OrderId = '1000';  -- Convert string sang int mỗi row
-- GOOD:
DELETE FROM Orders WHERE OrderId = 1000;    -- Type match, dùng index

-- 6. Với INSERT nhiều rows, dùng multi-row VALUES thay vì nhiều INSERT
-- BAD:
INSERT INTO Tags (Name) VALUES ('SQL');
INSERT INTO Tags (Name) VALUES ('Database');
INSERT INTO Tags (Name) VALUES ('Performance');

-- GOOD:
INSERT INTO Tags (Name) VALUES ('SQL'), ('Database'), ('Performance');

INSERT Performance

-- Tắt indexes trước khi BULK INSERT, bật lại sau
ALTER INDEX ALL ON Products DISABLE;

BULK INSERT Products
FROM 'D:\data\products.csv'
WITH (TABLOCK, BATCHSIZE = 10000);

ALTER INDEX ALL ON Products REBUILD;

-- Tắt CHECK và FK constraints để tăng tốc BULK INSERT
ALTER TABLE Products NOCHECK CONSTRAINT ALL;
-- ... BULK INSERT ...
ALTER TABLE Products WITH CHECK CHECK CONSTRAINT ALL;  -- Re-validate sau

-- Sử dụng minimal logging với SIMPLE recovery model
ALTER DATABASE MyDB SET RECOVERY SIMPLE;
-- ... BULK INSERT với TABLOCK ...
ALTER DATABASE MyDB SET RECOVERY FULL;

Truy vấn & Joins

Hiểu sâu về cách SQL Server xử lý JOIN và các loại truy vấn phức tạp là nền tảng để viết T-SQL hiệu quả và tối ưu hiệu suất.


1. INNER JOIN

INNER JOIN trả về các hàng có kết quả khớp ở cả hai bảng theo điều kiện join.

-- Cú pháp cơ bản
SELECT o.OrderId, o.OrderDate, c.CustomerName
FROM Orders o
INNER JOIN Customers c ON o.CustomerId = c.CustomerId;

-- Tương đương với (cú pháp cũ, không khuyến nghị)
SELECT o.OrderId, o.OrderDate, c.CustomerName
FROM Orders o, Customers c
WHERE o.CustomerId = c.CustomerId;

Lưu ý quan trọng

  • Hàng nào không có match ở bảng kia sẽ bị loại bỏ
  • Có thể join trên nhiều cột (composite key)
-- Join trên nhiều cột
SELECT *
FROM OrderDetails od
INNER JOIN Products p 
    ON od.ProductId = p.ProductId 
    AND od.WarehouseId = p.WarehouseId;

2. LEFT / RIGHT OUTER JOIN

LEFT JOIN (LEFT OUTER JOIN)

Trả về tất cả hàng từ bảng trái + hàng khớp từ bảng phải. Nếu không có match, cột bảng phải trả về NULL.

-- Lấy tất cả khách hàng, kể cả chưa có đơn hàng nào
SELECT c.CustomerId, c.CustomerName, o.OrderId, o.OrderDate
FROM Customers c
LEFT JOIN Orders o ON c.CustomerId = o.CustomerId;

-- Tìm khách hàng CHƯA có đơn hàng
SELECT c.CustomerId, c.CustomerName
FROM Customers c
LEFT JOIN Orders o ON c.CustomerId = o.CustomerId
WHERE o.OrderId IS NULL;   -- Anti-join pattern

RIGHT JOIN (RIGHT OUTER JOIN)

Trả về tất cả hàng từ bảng phải + hàng khớp từ bảng trái.

-- Tương đương với LEFT JOIN nhưng đổi thứ tự bảng
SELECT c.CustomerId, c.CustomerName, o.OrderId
FROM Orders o
RIGHT JOIN Customers c ON o.CustomerId = c.CustomerId;

Tip: Trong thực tế, hầu hết lập trình viên dùng LEFT JOIN hết, ít dùng RIGHT JOIN để code dễ đọc hơn.


3. FULL OUTER JOIN

Trả về tất cả hàng từ cả hai bảng. Nếu không có match, side không có sẽ là NULL.

-- Lấy tất cả employees và departments, kể cả không match
SELECT e.EmployeeId, e.Name, d.DepartmentName
FROM Employees e
FULL OUTER JOIN Departments d ON e.DepartmentId = d.DepartmentId;

-- Tìm các hàng chỉ tồn tại ở một trong hai bảng (symmetric difference)
SELECT e.EmployeeId, e.Name, d.DepartmentName
FROM Employees e
FULL OUTER JOIN Departments d ON e.DepartmentId = d.DepartmentId
WHERE e.EmployeeId IS NULL OR d.DepartmentId IS NULL;

4. CROSS JOIN

CROSS JOIN tạo ra tích Descartes (Cartesian product): mỗi hàng bảng A kết hợp với mọi hàng bảng B.

-- Tạo tất cả tổ hợp màu sắc và kích thước
SELECT c.ColorName, s.SizeName
FROM Colors c
CROSS JOIN Sizes s;
-- Nếu Colors có 5 hàng, Sizes có 3 hàng → 15 hàng kết quả

-- Ứng dụng thực tế: Tạo calendar/date dimension
SELECT d.DateValue, h.HourValue
FROM (VALUES ('2024-01-01'), ('2024-01-02'), ('2024-01-03')) AS d(DateValue)
CROSS JOIN (VALUES (0),(1),(2),(3),(4),(5),(6),(7),(8),(9),(10),(11)) AS h(HourValue);

Cảnh báo

CROSS JOIN với bảng lớn có thể tạo ra hàng triệu hàng, gây OutOfMemory hoặc timeout.


5. SELF JOIN

SELF JOIN là join bảng với chính nó, thường dùng cho dữ liệu phân cấp (hierarchical data).

-- Lấy employee và tên manager của họ
SELECT 
    e.EmployeeId,
    e.Name AS EmployeeName,
    m.Name AS ManagerName
FROM Employees e
LEFT JOIN Employees m ON e.ManagerId = m.EmployeeId;

-- Tìm các cặp nhân viên cùng phòng ban
SELECT 
    e1.Name AS Employee1,
    e2.Name AS Employee2,
    e1.DepartmentId
FROM Employees e1
INNER JOIN Employees e2 
    ON e1.DepartmentId = e2.DepartmentId
    AND e1.EmployeeId < e2.EmployeeId  -- Tránh duplicate pairs
ORDER BY e1.DepartmentId;

6. Multiple Table Joins (Kết hợp nhiều bảng)

-- JOIN 4 bảng
SELECT 
    o.OrderId,
    o.OrderDate,
    c.CustomerName,
    p.ProductName,
    od.Quantity,
    od.UnitPrice,
    cat.CategoryName
FROM Orders o
INNER JOIN Customers c    ON o.CustomerId = c.CustomerId
INNER JOIN OrderDetails od ON o.OrderId = od.OrderId
INNER JOIN Products p     ON od.ProductId = p.ProductId
INNER JOIN Categories cat ON p.CategoryId = cat.CategoryId
WHERE o.OrderDate >= '2024-01-01'
ORDER BY o.OrderDate DESC;

Thứ tự JOIN ảnh hưởng đến hiệu suất

SQL Server Optimizer thường chọn thứ tự tối ưu, nhưng có thể gợi ý thứ tự bằng OPTION (FORCE ORDER):

-- Ép optimizer join theo thứ tự viết
SELECT ...
FROM SmallTable s
INNER JOIN LargeTable l ON s.Id = l.SmallId
OPTION (FORCE ORDER);

7. Subqueries (Truy vấn con)

Non-Correlated Subquery

Subquery chạy một lần độc lập, không phụ thuộc outer query.

-- Lấy sản phẩm có giá cao hơn giá trung bình
SELECT ProductId, ProductName, Price
FROM Products
WHERE Price > (SELECT AVG(Price) FROM Products);

-- IN với subquery
SELECT * FROM Orders
WHERE CustomerId IN (
    SELECT CustomerId FROM Customers WHERE Country = 'Vietnam'
);

Correlated Subquery

Subquery tham chiếu đến outer query, chạy lại cho mỗi hàng.

-- Lấy đơn hàng gần nhất của mỗi khách hàng
SELECT o1.OrderId, o1.CustomerId, o1.OrderDate
FROM Orders o1
WHERE o1.OrderDate = (
    SELECT MAX(o2.OrderDate)
    FROM Orders o2
    WHERE o2.CustomerId = o1.CustomerId  -- Tham chiếu outer query
);

Performance Warning: Correlated subquery chạy N lần (N = số hàng outer query). Với bảng lớn, nên viết lại thành JOIN hoặc dùng window function.

-- ✅ Viết lại với ROW_NUMBER() - hiệu quả hơn
WITH LatestOrders AS (
    SELECT *, ROW_NUMBER() OVER (PARTITION BY CustomerId ORDER BY OrderDate DESC) AS rn
    FROM Orders
)
SELECT OrderId, CustomerId, OrderDate
FROM LatestOrders
WHERE rn = 1;

Scalar Subquery

Subquery trả về đúng một giá trị (một hàng, một cột).

SELECT 
    ProductId,
    ProductName,
    Price,
    (SELECT AVG(Price) FROM Products) AS AvgPrice,
    Price - (SELECT AVG(Price) FROM Products) AS DiffFromAvg
FROM Products;

8. EXISTS vs IN vs JOIN

EXISTS

Kiểm tra sự tồn tại - dừng ngay khi tìm thấy hàng đầu tiên.

-- Khách hàng có ít nhất 1 đơn hàng
SELECT c.CustomerId, c.CustomerName
FROM Customers c
WHERE EXISTS (
    SELECT 1
    FROM Orders o
    WHERE o.CustomerId = c.CustomerId
);

IN

So sánh giá trị với một tập hợp kết quả.

SELECT ProductId, ProductName
FROM Products
WHERE CategoryId IN (1, 3, 5);

SELECT ProductId, ProductName
FROM Products
WHERE CategoryId IN (SELECT CategoryId FROM Categories WHERE IsActive = 1);

So sánh hiệu suất

Cách dùngKhi nào dùngLưu ý
EXISTSSubquery bảng lớn, chỉ cần kiểm tra tồn tạiDừng sớm khi tìm thấy match
INTập giá trị nhỏ/tĩnhCó thể chậm nếu subquery trả nhiều hàng
JOINCần lấy dữ liệu từ bảng joinOptimizer thường tối ưu tốt nhất
NOT INTránh dùng khi có NULLNULL trong tập hợp khiến toàn bộ kết quả FALSE
NOT EXISTSAnti-join an toàn với NULLKhuyến nghị thay thế NOT IN
-- ❌ NOT IN nguy hiểm với NULL
SELECT * FROM Orders
WHERE CustomerId NOT IN (SELECT CustomerId FROM BlacklistCustomers);
-- Nếu BlacklistCustomers có 1 hàng CustomerId = NULL → không hàng nào được trả về!

-- ✅ NOT EXISTS an toàn hơn
SELECT * FROM Orders o
WHERE NOT EXISTS (
    SELECT 1 FROM BlacklistCustomers bc WHERE bc.CustomerId = o.CustomerId
);

9. APPLY Operator

APPLY gọi một table-valued function hoặc subquery cho mỗi hàng của outer table.

CROSS APPLY

Giống INNER JOIN: chỉ trả về hàng outer khi subquery có kết quả.

-- Top 3 orders gần nhất cho mỗi khách hàng
SELECT c.CustomerId, c.CustomerName, o.OrderId, o.OrderDate
FROM Customers c
CROSS APPLY (
    SELECT TOP 3 OrderId, OrderDate
    FROM Orders
    WHERE CustomerId = c.CustomerId
    ORDER BY OrderDate DESC
) o;

-- Sử dụng với Table-Valued Function
SELECT e.EmployeeId, t.SaleAmount
FROM Employees e
CROSS APPLY dbo.GetTopSalesForEmployee(e.EmployeeId, 5) t;

OUTER APPLY

Giống LEFT JOIN: trả về tất cả hàng outer, dù subquery không có kết quả (NULL).

-- Tất cả khách hàng, kể cả chưa có đơn hàng
SELECT c.CustomerId, c.CustomerName, o.OrderId, o.OrderDate
FROM Customers c
OUTER APPLY (
    SELECT TOP 1 OrderId, OrderDate
    FROM Orders
    WHERE CustomerId = c.CustomerId
    ORDER BY OrderDate DESC
) o;

APPLY vs JOIN

-- CROSS APPLY có thể làm những việc mà JOIN không làm được:
-- Gọi TVF với tham số từ outer table
SELECT p.ProductId, p.ProductName, r.RelatedProductId
FROM Products p
CROSS APPLY dbo.GetRelatedProducts(p.ProductId, p.CategoryId) r;

10. Set Operations: UNION / UNION ALL / INTERSECT / EXCEPT

UNION vs UNION ALL

-- UNION: loại bỏ duplicate (tốn thêm chi phí DISTINCT)
SELECT ProductId, ProductName FROM ActiveProducts
UNION
SELECT ProductId, ProductName FROM ArchivedProducts;

-- UNION ALL: giữ tất cả hàng, nhanh hơn
SELECT ProductId, ProductName FROM ActiveProducts
UNION ALL
SELECT ProductId, ProductName FROM ArchivedProducts;

INTERSECT

Trả về hàng có trong cả hai tập hợp.

-- Sản phẩm vừa có trong ActiveProducts vừa có trong FeaturedProducts
SELECT ProductId, ProductName FROM ActiveProducts
INTERSECT
SELECT ProductId, ProductName FROM FeaturedProducts;

EXCEPT

Trả về hàng có trong tập đầu nhưng không có ở tập sau.

-- Sản phẩm active nhưng chưa được featured
SELECT ProductId, ProductName FROM ActiveProducts
EXCEPT
SELECT ProductId, ProductName FROM FeaturedProducts;

Lưu ý

  • Số cột và kiểu dữ liệu phải tương thích
  • UNION / INTERSECT / EXCEPT đều loại bỏ duplicate
  • UNION ALL là ngoại lệ duy nhất không loại duplicate
  • ORDER BY chỉ được phép ở cuối cùng
SELECT ProductId, ProductName, 'Active' AS Source FROM ActiveProducts
UNION ALL
SELECT ProductId, ProductName, 'Archived' AS Source FROM ArchivedProducts
ORDER BY ProductName;  -- ORDER BY chỉ ở cuối cùng

11. Derived Tables (Inline Views)

Derived table là subquery trong mệnh đề FROM, hoạt động như một bảng tạm thời.

-- Derived table: lấy top customers theo doanh thu
SELECT dt.CustomerId, dt.CustomerName, dt.TotalRevenue
FROM (
    SELECT 
        c.CustomerId,
        c.CustomerName,
        SUM(o.TotalAmount) AS TotalRevenue
    FROM Customers c
    INNER JOIN Orders o ON c.CustomerId = o.CustomerId
    GROUP BY c.CustomerId, c.CustomerName
) AS dt   -- Bắt buộc phải đặt alias
WHERE dt.TotalRevenue > 10000
ORDER BY dt.TotalRevenue DESC;

CTE vs Derived Table

-- CTE (Common Table Expression) - thường dễ đọc hơn
WITH CustomerRevenue AS (
    SELECT 
        c.CustomerId,
        c.CustomerName,
        SUM(o.TotalAmount) AS TotalRevenue
    FROM Customers c
    INNER JOIN Orders o ON c.CustomerId = o.CustomerId
    GROUP BY c.CustomerId, c.CustomerName
)
SELECT * FROM CustomerRevenue WHERE TotalRevenue > 10000;

Note: CTE và Derived table về mặt hiệu suất thường tương đương - optimizer xử lý chúng giống nhau. CTE có thể tái sử dụng trong cùng query.


12. Query Hints (Gợi ý truy vấn)

NOLOCK (READ UNCOMMITTED)

-- Đọc dữ liệu chưa được commit (dirty read), tránh blocking
SELECT * FROM Orders WITH (NOLOCK)
WHERE OrderDate >= '2024-01-01';

-- Cảnh báo: có thể đọc dữ liệu "phantom" hoặc dữ liệu bị rollback!

FORCESEEK

-- Ép optimizer dùng Index Seek thay vì Scan
SELECT * FROM Orders WITH (FORCESEEK)
WHERE CustomerId = 12345;

-- Ép seek trên index cụ thể
SELECT * FROM Orders WITH (FORCESEEK (IX_Orders_CustomerId (CustomerId)))
WHERE CustomerId = 12345;

INDEX hint

-- Ép dùng index cụ thể
SELECT * FROM Orders WITH (INDEX = IX_Orders_OrderDate)
WHERE OrderDate BETWEEN '2024-01-01' AND '2024-12-31';

-- Ép Table Scan (bỏ qua tất cả indexes)
SELECT * FROM SmallTable WITH (INDEX = 0);

OPTION hints

-- Force recompile plan cho query này
SELECT * FROM Orders WHERE CustomerId = @Id 
OPTION (RECOMPILE);

-- Giới hạn parallelism
SELECT * FROM BigTable 
OPTION (MAXDOP 4);

-- Optimize for specific parameter value
SELECT * FROM Orders WHERE CustomerId = @Id
OPTION (OPTIMIZE FOR (@Id = 1000));

-- Optimize for unknown (avoid parameter sniffing)
SELECT * FROM Orders WHERE CustomerId = @Id
OPTION (OPTIMIZE FOR UNKNOWN);

Best practice: Chỉ dùng hints khi bạn biết rõ lý do - hints có thể trở nên không còn tối ưu khi data distribution thay đổi.


Q&A - Phỏng vấn

Junior Level

Q: Sự khác biệt giữa INNER JOIN và LEFT JOIN là gì?

A: INNER JOIN chỉ trả về hàng có match ở cả hai bảng. LEFT JOIN trả về tất cả hàng bảng trái, nếu không có match bảng phải thì các cột đó là NULL. Dùng LEFT JOIN khi cần giữ lại tất cả records của bảng chính dù không có dữ liệu liên quan.


Q: UNION khác UNION ALL như thế nào?

A: UNION loại bỏ các hàng trùng lặp (thực hiện DISTINCT ngầm), UNION ALL giữ tất cả hàng kể cả duplicate. UNION chậm hơn vì phải so sánh để loại duplicate. Dùng UNION ALL khi biết chắc không có duplicate hoặc không quan tâm đến duplicate để tăng hiệu suất.


Q: Subquery là gì? Cho ví dụ?

A: Subquery là truy vấn SQL nằm bên trong một truy vấn khác (trong WHERE, FROM, SELECT, HAVING). Ví dụ: SELECT * FROM Products WHERE Price > (SELECT AVG(Price) FROM Products) - subquery tính giá trung bình, outer query lọc sản phẩm đắt hơn trung bình.


Mid Level

Q: Khi nào nên dùng EXISTS thay vì IN?

A: Dùng EXISTS khi subquery trả về nhiều hàng (bảng lớn) vì EXISTS dừng ngay khi tìm thấy một hàng match (short-circuit). NOT IN phải tránh nếu tập có thể chứa NULL vì kết quả sẽ luôn rỗng. NOT EXISTS là lựa chọn an toàn hơn. Với tập giá trị nhỏ và tĩnh thì IN vẫn dùng được.


Q: Correlated subquery là gì? Tại sao có thể gây vấn đề hiệu suất?

A: Correlated subquery tham chiếu đến cột của outer query, nghĩa là nó được thực thi lại cho mỗi hàng của outer query. Nếu outer query trả về N hàng thì subquery chạy N lần. Với bảng lớn điều này rất tốn kém. Giải pháp: viết lại bằng JOIN hoặc window functions (ROW_NUMBER, RANK…).


Q: CROSS APPLY và OUTER APPLY khác nhau thế nào?

A: CROSS APPLY tương tự INNER JOIN - chỉ trả về outer rows khi subquery/TVF có kết quả. OUTER APPLY tương tự LEFT JOIN - trả về tất cả outer rows, nếu không có match thì cột bên trong là NULL. APPLY thường dùng để gọi table-valued function với tham số từ outer table, hoặc để lấy top-N rows per group.


Q: Tại sao NOT IN nguy hiểm khi có NULL trong tập hợp?

A: Vì SQL dùng logic ba giá trị (TRUE/FALSE/UNKNOWN). Khi so sánh bất kỳ giá trị nào với NULL, kết quả là UNKNOWN. Trong NOT IN, nếu tập chứa NULL thì điều kiện x NOT IN (..., NULL, ...) tương đương x != NULL là UNKNOWN, khiến không hàng nào được trả về. Luôn dùng NOT EXISTS thay thế.


Senior Level

Q: Giải thích cơ chế Join Algorithms của SQL Server (Nested Loops, Hash Join, Merge Join)?

A:

  • Nested Loops: Với mỗi hàng outer table, duyệt qua inner table để tìm match. Hiệu quả khi outer table nhỏ và inner table có index trên join key. O(N×M) worst case.
  • Hash Join: Build hash table từ bảng nhỏ hơn (build input), sau đó probe với bảng lớn hơn. Hiệu quả cho large, unsorted, unindexed inputs. Tốn bộ nhớ - có thể spill ra TempDB.
  • Merge Join: Cả hai inputs phải được sắp xếp theo join key. Rất hiệu quả khi inputs đã có order (từ index). O(N+M). Optimizer chọn dựa trên ước tính cardinality và có index hay không.

Q: Làm thế nào để viết lại correlated subquery lấy top-N per group hiệu quả hơn trong SQL Server?

A: Dùng window functions thay vì correlated subquery:

-- ❌ Correlated subquery - chạy N lần
SELECT * FROM Orders o1
WHERE OrderDate IN (
    SELECT TOP 3 OrderDate FROM Orders o2
    WHERE o2.CustomerId = o1.CustomerId
    ORDER BY OrderDate DESC
);

-- ✅ ROW_NUMBER() - hiệu quả hơn nhiều
WITH RankedOrders AS (
    SELECT *, ROW_NUMBER() OVER (
        PARTITION BY CustomerId 
        ORDER BY OrderDate DESC
    ) AS rn
    FROM Orders
)
SELECT * FROM RankedOrders WHERE rn <= 3;

Window function tiếp cận theo set-based, SQL Server có thể tối ưu tốt hơn nhiều so với row-by-row correlated subquery.


Q: Khi nào APPLY operator tốt hơn JOIN?

A: APPLY tốt hơn JOIN trong các trường hợp:

  1. TVF với tham số từ outer row: CROSS APPLY dbo.fn(e.EmployeeId) - JOIN không thể làm điều này.
  2. Top-N per group: CROSS APPLY (SELECT TOP 3 ... WHERE CustomerId = c.CustomerId ORDER BY ...) - thường hiệu quả hơn ROW_NUMBER() cho cardinality thấp.
  3. Tránh outer reference trong subquery phức tạp: APPLY cho phép tái sử dụng alias trong cùng mệnh đề APPLY.

JOIN thường được optimizer tối ưu tốt hơn khi chỉ cần equi-join đơn giản.

Indexes & Performance

Phần này bao gồm tất cả kiến thức về Index và tối ưu hiệu suất trong SQL Server - từ cơ bản đến nâng cao.

Các chủ đề

  • Index Cơ Bản: Clustered/Non-Clustered Index, B-tree, SARGable predicates, chọn clustered key, covering index, fragmentation, index maintenance
  • Index Nâng Cao: Filtered Index, Columnstore Index, Full-Text, XML, Spatial, Unique, Composite, In-Memory OLTP Indexes
  • Query Optimization: Query Pipeline, Cost-Based Optimizer, Anti-Patterns, JOIN Algorithms, Parameter Sniffing, TempDB, Parallelism, DMVs, Extended Events
  • Execution Plans: Đọc và phân tích execution plans, các operators phổ biến

Q&A - Phỏng vấn

Junior Level

Q1: Index là gì và tại sao cần dùng index?

A: Index là cấu trúc dữ liệu bổ sung (B-tree) giúp SQL Server tìm kiếm dữ liệu nhanh hơn mà không cần đọc toàn bộ bảng (Table Scan). Không có index, SQL Server phải đọc tất cả các trang dữ liệu. Với index, SQL Server đi theo B-tree từ root đến leaf để tìm đúng records - tương tự mục lục của cuốn sách.


Q2: Clustered Index và Non-Clustered Index khác nhau thế nào?

A:

  • Clustered Index: quyết định thứ tự vật lý lưu trữ dữ liệu trong bảng. Leaf pages chứa chính data rows. Mỗi bảng chỉ có 1 clustered index.
  • Non-Clustered Index: cấu trúc B-tree riêng biệt với bảng. Leaf pages chứa key columns + row locator (clustered key hoặc RID nếu bảng là heap). Mỗi bảng có thể có đến 999 non-clustered indexes.

Q3: Khi nào xảy ra Table Scan và khi nào Index Seek?

A: Table Scan xảy ra khi không có index phù hợp, predicate không SARGable, hoặc optimizer ước tính phần lớn bảng sẽ được đọc (thường > 5-30%). Index Seek xảy ra khi có SARGable predicate matching leading key column của index. SARGable có nghĩa là cột không bị bao bọc trong function hay biểu thức.


Q4: Covering Index là gì?

A: Covering Index là index chứa tất cả các cột mà một query cần - cả cột dùng để filter (WHERE) và cột cần lấy (SELECT). Khi query được “covered” bởi index, SQL Server không cần quay lại bảng gốc để lấy thêm dữ liệu (tránh Key Lookup), giúp giảm I/O đáng kể.


Q5: UNION và UNION ALL khác nhau thế nào?

A: UNION loại bỏ duplicate rows (thực hiện DISTINCT ngầm, tốn thêm sort/hash). UNION ALL giữ tất cả rows kể cả duplicate và nhanh hơn. Dùng UNION ALL khi biết chắc không có duplicate hoặc không cần loại bỏ chúng để tối ưu hiệu suất.


Q6: Fill Factor là gì? Giá trị nên chọn là bao nhiêu?

A: Fill Factor là % không gian lấp đầy trên mỗi leaf page khi tạo/rebuild index. Để lại không gian trống giúp giảm page split khi INSERT. Giá trị phổ biến:

  • 100 (0): read-only tables
  • 80-90: mixed workload bình thường
  • 60-70: heavy insert/update vào giữa index

Q7: Heap là gì trong SQL Server?

A: Heap là bảng không có clustered index - data pages không được tổ chức theo thứ tự. SQL Server dùng IAM (Index Allocation Map) pages để tìm data pages. Heap tốt khi insert nhiều và không cần range scan. Non-clustered index trên Heap dùng RID (Row ID = file + page + slot) làm row locator thay vì clustered key.


Q8: Fragmentation là gì? Khi nào REBUILD vs REORGANIZE?

A: Fragmentation xảy ra khi pages không còn liên tục về mặt vật lý sau nhiều INSERT/UPDATE/DELETE. Ảnh hưởng: sequential I/O trở thành random I/O, chậm hơn.

  • REORGANIZE: fragmentation 10-30%, online (không block reads/writes), ít log hơn
  • REBUILD: fragmentation > 30%, có thể offline hoặc ONLINE (Enterprise), reset fill factor, update statistics đầy đủ

Mid Level

Q9: Tại sao không nên tạo quá nhiều indexes trên một bảng?

A: Mỗi index tốn thêm write overhead: khi INSERT/UPDATE/DELETE một hàng, SQL Server phải cập nhật tất cả indexes liên quan. Ngoài ra, nhiều indexes tốn storage và RAM (buffer pool). Index cũng cần maintenance (REBUILD/REORGANIZE). Rule of thumb: OLTP tables thường không nên quá 5-7 indexes, DWH/fact tables có thể nhiều hơn.


Q10: SARGable predicate là gì? Cho ví dụ non-SARGable và cách sửa?

A: SARGable (Search ARGument ABLE) là predicate mà SQL Server có thể dùng để Index Seek. Predicate trở thành non-SARGable khi cột nằm bên trong function hoặc biểu thức.

-- ❌ Non-SARGable
WHERE YEAR(OrderDate) = 2024
WHERE UPPER(LastName) = 'NGUYEN'

-- ✅ SARGable alternatives
WHERE OrderDate >= '2024-01-01' AND OrderDate < '2025-01-01'
WHERE LastName = 'Nguyen'   -- CI collation tự xử lý case-insensitive

Q11: Parameter Sniffing là gì? Khi nào nó gây vấn đề và cách giải quyết?

A: Parameter Sniffing xảy ra khi SQL Server compile stored procedure lần đầu với một parameter value cụ thể và cache plan đó. Nếu distribution data không đồng đều, plan tốt cho một giá trị có thể rất tệ cho giá trị khác.

Giải pháp:

  1. OPTION (RECOMPILE) - compile lại mỗi lần (mất cache benefit)
  2. OPTION (OPTIMIZE FOR UNKNOWN) - dùng average statistics
  3. Local variable trick - DECLARE @Local = @Param
  4. Query Store - detect regression và force good plan

Q12: Composite Index: thứ tự cột ảnh hưởng thế nào?

A: SQL Server chỉ có thể seek trên composite index nếu bắt đầu từ leading column. Index (A, B, C) hỗ trợ seek trên A, A+B, A+B+C nhưng không seek được trên chỉ B hay C. Nguyên tắc: equality columns trước, range column sau cùng, selectivity cao ưu tiên trước.


Q13: Khi nào dùng Filtered Index?

A: Filtered Index hữu ích khi:

  1. Sparse data: cột có nhiều NULL (chỉ index hàng không NULL)
  2. Soft deletes: WHERE IsDeleted = 0 - chỉ index active records
  3. Status filtering: chỉ query một trạng thái cụ thể thường xuyên
  4. Partial uniqueness: unique email chỉ trong active users

Filtered Index nhỏ hơn full index → ít RAM, ít I/O, ít maintenance. Nhưng chỉ được dùng khi query predicate tương thích với filter predicate của index.


Q14: Hash Join vs Nested Loops vs Merge Join - khi nào optimizer chọn cái nào?

A:

  • Nested Loops: outer table nhỏ, inner table có index trên join key. Tốt cho OLTP lookups.
  • Hash Join: cả hai inputs lớn, không có index phù hợp. Tốn memory (có thể spill TempDB).
  • Merge Join: cả hai inputs đã sorted theo join key (từ index). Rất hiệu quả nhưng cần sorted input.

Optimizer chọn dựa trên estimated row counts và available indexes. Nếu estimates sai (statistics lỗi thời), wrong algorithm có thể được chọn.


Q15: Implicit conversion là gì và tại sao gây vấn đề hiệu suất?

A: Implicit conversion xảy ra khi SQL Server tự động convert kiểu dữ liệu để so sánh. Ví dụ: cột NVARCHAR so sánh với VARCHAR parameter → SQL Server phải convert toàn bộ cột sang VARCHAR, làm mất khả năng dùng index. Giải pháp: đảm bảo parameter và column có cùng kiểu dữ liệu, dùng N'' prefix cho Unicode strings.


Q16: Covering Index và Key Lookup liên quan thế nào?

A: Key Lookup xảy ra khi SQL Server tìm rows qua Non-Clustered Index nhưng cần thêm cột không có trong index → phải quay về Clustered Index/Heap để lấy. Đây là Bookmark Lookup. Mỗi Key Lookup là một round-trip tốn kém. Covering Index loại bỏ Key Lookup bằng cách thêm các cột cần thiết vào INCLUDE.


Q17: Columnstore Index phù hợp cho workload nào?

A: Columnstore Index tối ưu cho OLAP/analytical workloads: aggregations (SUM, AVG, COUNT), GROUP BY, wide table scans. Lý do: compression tốt hơn (5-10x), batch mode execution, column pruning (chỉ đọc cột cần). Không phù hợp cho OLTP với nhiều point lookups và single-row updates/inserts (delta store overhead).


Senior Level

Q18: Giải thích cơ chế Plan Cache và khi nào plan bị evict hoặc recompile?

A: SQL Server lưu execution plans trong Plan Cache (một phần của buffer pool). Plan được identify bằng hash của query text. Khi query mới match hash, dùng lại plan (avoid expensive compilation).

Plan bị evict khi:

  • Memory pressure (cần RAM cho dữ liệu)
  • DBCC FREEPROCCACHE
  • Đủ điều kiện memory pressure eviction (least recently used)

Plan bị recompile khi:

  • Schema change (ALTER TABLE, CREATE INDEX)
  • Statistics change (threshold: 20% rows + 500 modified)
  • SET options thay đổi
  • sp_recompile hoặc OPTION (RECOMPILE) hint

Q19: Giải thích Columnstore Delta Store và Tuple Mover hoạt động thế nào?

A: Khi INSERT vào Columnstore table:

  1. Rows mới đi vào Delta Store (row-store B-tree nhỏ trong TempDB/data file)
  2. Delta Store tích lũy đến ~1 triệu rows
  3. Tuple Mover (background process) compress và chuyển Delta Store → Column Segments (read-only, highly compressed)
  4. Deleted rows được đánh dấu trong Delete Bitmap (không xóa ngay khỏi segments)

Khi REBUILD Columnstore Index → Delta Stores và Delete Bitmaps được flush, tất cả merge thành segments mới, reclaim không gian.


Q20: Wait statistics là gì? Cách dùng để diagnose performance vấn đề?

A: SQL Server track thời gian mỗi task phải chờ resource (sys.dm_os_wait_stats). Phân tích top waits giúp xác định bottleneck:

  • PAGEIOLATCH: I/O bottleneck → thêm RAM, optimize queries, faster disk
  • LCK_M_X: lock contention → shorten transactions, optimize isolation level
  • CXPACKET: parallelism overhead → tăng cost threshold, giảm MAXDOP
  • ASYNCH_NETWORK_IO: client chậm đọc → reduce result set, pagination
  • SOS_SCHEDULER_YIELD: CPU pressure → optimize CPU-heavy queries, thêm CPU

Approach: reset waits (DBCC SQLPERF('sys.dm_os_wait_stats', CLEAR)), run workload 30 phút, check top waits lại → targeted investigation.


Q21: Hash Join Spill là gì và làm thế nào để tránh?

A: Hash Join Spill xảy ra khi SQL Server không có đủ memory để giữ hash table trong RAM → phải spill sang TempDB (disk). Điều này tăng I/O dramatically và làm query chậm.

Nguyên nhân:

  1. Cardinality estimate quá thấp (statistics lỗi thời) → cấp ít memory grant
  2. Query thực sự xử lý dữ liệu rất lớn

Giải pháp:

  1. Update statistics
  2. Thêm/sửa indexes để reduce rows trước khi join
  3. Tăng memory grant hint: OPTION (MIN_GRANT_PERCENT = 50)
  4. Tăng server memory
  5. Dùng Query Store để detect và force better plan

Q22: Query Store có gì tốt hơn Extended Events để track performance?

A: Query Store (SQL 2016+) tự động persist query text, plans và runtime stats vào database catalog - không cần setup trace session. Cho phép:

  • So sánh plan thay đổi theo thời gian (plan regression detection)
  • Force plan cụ thể khi có regression
  • Identify “regressed queries” tự động (SQL 2022 Intelligent Query Processing)
  • View qua SSMS GUI hoặc sys.query_store_* views

Extended Events tốt hơn khi cần real-time capture, filter chi tiết (theo database, user, host), capture events không có trong Query Store (locking, deadlock, etc.).


Q23: In-Memory OLTP indexes khác traditional indexes thế nào?

A:

  • Hash Index: dùng cho equality lookups, O(1), không sort. PHẢI ước tính BUCKET_COUNT chính xác (too low → collision, too high → wasted memory). Không hỗ trợ range, ORDER BY.
  • Nonclustered Range Index: Bw-tree (lock-free B-tree variant). Hỗ trợ range và ORDER BY. O(log N).
  • Không có clustered index: Memory-optimized tables không có clustered index concept. Data là heap trong memory, access qua indexes.
  • Lock-free: All indexes dùng optimistic concurrency, MVCC - không có page latches hay traditional locks.

Q24: Giải thích MAXDOP và Cost Threshold for Parallelism, nên set về giá trị bao nhiêu?

A:

  • MAXDOP (Max Degree of Parallelism): số CPU threads tối đa cho một query. Default 0 = tất cả CPUs.
  • Cost Threshold for Parallelism: estimated cost threshold để SQL Server mới xem xét parallel plan. Default 5 (rất thấp, gây nhiều small queries đi parallel không cần thiết).

Recommendations:

  • Cost Threshold: tăng lên 25-50 cho OLTP workloads (giảm unnecessary parallelism overhead)
  • MAXDOP OLTP: = số cores per NUMA node hoặc 8 (lấy số nhỏ hơn)
  • MAXDOP DWH: 0 hoặc số cores per socket

Từ SQL Server 2022: ALTER DATABASE SCOPED CONFIGURATION cho phép set per-database.


Q25: Tại sao NOT IN nguy hiểm khi có NULL? Giải thích bằng SQL logic ba giá trị?

A: SQL dùng three-valued logic: TRUE, FALSE, và UNKNOWN. Khi so sánh với NULL, kết quả là UNKNOWN (không phải FALSE).

x NOT IN (1, 2, NULL) được evaluate như: NOT (x = 1 OR x = 2 OR x = NULL) = NOT (x = 1 OR x = 2 OR UNKNOWN) = NOT (UNKNOWN) (khi x ≠ 1 và x ≠ 2) = UNKNOWN

WHERE chỉ cho qua rows với UNKNOWN = TRUE. UNKNOWN rows bị loại. Kết quả: không hàng nào được trả về dù logic bạn muốn là “x không có trong danh sách”. Luôn dùng NOT EXISTS thay thế.


Q26: Giải thích cơ chế Statistics trong SQL Server và khi nào bị outdated?

A: Statistics là histogram mô tả phân phối dữ liệu của một column. SQL Server dùng statistics để ước tính cardinality (số rows mỗi operator trả về), từ đó chọn join algorithm, index, và memory grant.

Statistics tự động update khi:

  • 20% của rows trong bảng bị modify (+ 500 rows cho bảng nhỏ)
  • Với trace flag 2371 hoặc SQL 2016+: dynamic threshold cho bảng lớn

Statistics có thể không đủ chính xác khi:

  1. Data distribution lệch (skewed) - histogram chỉ có 200 steps
  2. Nhiều NULL values
  3. Correlated columns (stats độc lập nhau, không track correlation)
  4. Ascending key problem (new values ngoài histogram range)

Giải pháp: UPDATE STATISTICS ... WITH FULLSCAN, enable Auto Update Statistics Async.


Q27: Concurrent index rebuild có thể làm gì? Hạn chế là gì?

A: ALTER INDEX ... REBUILD WITH (ONLINE = ON) (Enterprise Edition) cho phép rebuild không block reads/writes:

  • Dùng row versioning để maintain cả hai copies trong quá trình rebuild
  • Schema Modification Lock (SCH-M) chỉ lấy ở đầu và cuối (rất ngắn)
  • Tốn thêm disk space (phải giữ cả old và new index cùng lúc)
  • Chậm hơn offline rebuild
  • Không available trên Standard Edition
  • Không available cho Columnstore indexes (dùng REORGANIZE hoặc offline)

Q28: Giải thích sự khác biệt giữa Seek Predicate và Predicate trong execution plan?

A:

  • Seek Predicate: phần predicate SQL Server dùng để navigate B-tree (true seek). Rất hiệu quả - chỉ đọc relevant leaf pages.
  • Predicate (Residual predicate): phần predicate được apply sau khi đã seek đến leaf pages, để filter thêm các rows đọc được. Kém hiệu quả hơn vì phải đọc rồi mới filter.

Ví dụ với Index (CustomerId, Status):

WHERE CustomerId = 100 AND Status = 'Active' AND TotalAmount > 500
-- Seek Predicate: CustomerId = 100 AND Status = 'Active' (cả hai key columns)
-- Residual Predicate: TotalAmount > 500 (không phải key column)

Mục tiêu tối ưu: đưa càng nhiều predicate vào Seek Predicate càng tốt (thêm cột vào index key hoặc adjust index).


Q29: Khi nào nên dùng Table Variable thay Temp Table và ngược lại?

A: Table Variable tốt hơn khi:

  • < 100 rows (thường)
  • Trong function (bắt buộc dùng table variable)
  • Cần transactions riêng (không bị rollback khi outer TX rollback)
  • Đơn giản, không cần indexes phức tạp

Temp Table tốt hơn khi:

  • 1000 rows (statistics giúp optimizer)

  • Cần explicit indexes
  • JOIN phức tạp với temp data (optimizer có statistics ước tính đúng hơn)
  • Cần dùng trong nhiều batches/procedures

Vấn đề chính của Table Variable: optimizer luôn ước tính 1 row (SQL 2017 trở lên có Deferred Compilation cải thiện phần nào). Điều này gây wrong join algorithm nếu thực tế có nhiều rows.


Q30: Làm thế nào phân tích và fix một query đột ngột chạy chậm trên production?

A: Quy trình điều tra:

  1. Query Store (SQL 2016+): kiểm tra “Regressed Queries” trong SSMS hoặc:
SELECT q.query_id, qt.query_sql_text, rs.avg_duration, p.plan_id
FROM sys.query_store_query q
INNER JOIN sys.query_store_query_text qt ON q.query_text_id = qt.query_text_id
INNER JOIN sys.query_store_plan p ON q.query_id = p.query_id
INNER JOIN sys.query_store_runtime_stats rs ON p.plan_id = rs.plan_id
WHERE rs.last_execution_time > DATEADD(HOUR, -2, GETUTCDATE())
ORDER BY rs.avg_duration DESC;
  1. So sánh plans: xem plan mới vs plan cũ có gì khác (join algorithm thay đổi, index mới/bị xóa)

  2. Check statistics: sys.dm_db_stats_properties - xem modification_counter cao → update statistics

  3. Check blocking/locks: sys.dm_exec_requests với blocking_session_id

  4. Check execution plan hiện tại: sys.dm_exec_query_plan - có key lookup không cần thiết, bad join không?

  5. Quick fix: EXEC sp_recompile 'ProcName' hoặc force plan qua Query Store

  6. Root cause: update statistics, thêm/sửa index, fix parameter sniffing, hoặc code fix

Index Cơ Bản

Index là cấu trúc dữ liệu đặc biệt giúp SQL Server tìm kiếm và truy cập dữ liệu nhanh hơn nhiều so với quét toàn bộ bảng. Hiểu index đúng là yếu tố quyết định hiệu suất của ứng dụng database.


1. Tại sao cần Index?

Không có index, SQL Server phải thực hiện Table Scan - đọc toàn bộ mọi trang dữ liệu của bảng từ đầu đến cuối. Với bảng hàng triệu hàng, điều này cực kỳ tốn kém.

B-tree Structure

SQL Server dùng cấu trúc B-tree (Balanced Tree) cho hầu hết loại index:

                    [Root Page]
                   /     |      \
          [Branch]   [Branch]   [Branch]
          /    \     /    \     /    \
       [Leaf] [Leaf] [Leaf] [Leaf] [Leaf] [Leaf]
         ↓      ↓      ↓      ↓      ↓      ↓
       Data   Data   Data   Data   Data   Data
  • Root page: điểm xuất phát tìm kiếm
  • Branch pages: định hướng tìm kiếm
  • Leaf pages: chứa dữ liệu thực (clustered) hoặc row pointer (non-clustered)

Seek vs Scan

Thao tácMô tảChi phí
Index SeekĐi qua B-tree để đến đúng leaf pageO(log N) - rất nhanh
Index ScanĐọc toàn bộ leaf pages của indexO(N) - chậm hơn
Table ScanĐọc toàn bộ bảng (heap hoặc clustered index)O(N) - chậm nhất

2. Clustered Index

Khái niệm

Clustered Index quyết định thứ tự vật lý lưu trữ dữ liệu trong bảng. Leaf pages của clustered index chứa chính data rows.

  • Mỗi bảng chỉ có tối đa 1 clustered index
  • Khi tạo PRIMARY KEY, SQL Server mặc định tạo clustered index trên đó
-- Tạo clustered index tường minh
CREATE CLUSTERED INDEX CIX_Orders_OrderId ON Orders (OrderId);

-- PRIMARY KEY tự động tạo clustered index
CREATE TABLE Orders (
    OrderId INT PRIMARY KEY,  -- → Clustered index tự động
    OrderDate DATETIME,
    CustomerId INT
);

-- Tạo PRIMARY KEY nhưng NON-clustered
CREATE TABLE Orders (
    OrderId INT PRIMARY KEY NONCLUSTERED,  -- Primary key không phải clustered
    OrderDate DATETIME,
    CustomerId INT
);

Heap vs Clustered Table

Heap (không có clustered index)Clustered Table
Lưu trữIAM + data pages không có thứ tựB-tree có thứ tự
InsertNhanh (thêm vào bất kỳ đâu)Có thể gây page split
Range scanChậm (random I/O)Nhanh (sequential I/O)
Lookup từ NCXRID lookup (kém hơn)Key lookup
Khi nào dùngStaging tables, bulk insertHầu hết trường hợp
-- Kiểm tra bảng có clustered index không (heap nếu type_desc = HEAP)
SELECT 
    t.name AS TableName,
    i.name AS IndexName,
    i.type_desc
FROM sys.tables t
LEFT JOIN sys.indexes i ON t.object_id = i.object_id AND i.type <= 1
ORDER BY t.name;

Chọn Clustered Index Key tốt

Một clustered index key tốt cần:

Tiêu chíLý do
Narrow (hẹp)Key được copy vào mọi non-clustered index → key lớn = tăng kích thước tất cả NCX
UniqueNếu không unique, SQL Server tự thêm 4-byte uniquifier
Ever-increasingINSERT luôn vào cuối → tránh page split → IDENTITY hoặc NEWSEQUENTIALID()
StaticCập nhật clustered key → phải di chuyển hàng dữ liệu
-- ✅ Tốt: INT IDENTITY - hẹp, unique, tăng dần
CREATE TABLE Orders (
    OrderId INT IDENTITY(1,1) PRIMARY KEY,
    ...
);

-- ❌ Kém: GUID ngẫu nhiên - random insert gây fragmentation cao
CREATE TABLE Orders (
    OrderId UNIQUEIDENTIFIER DEFAULT NEWID() PRIMARY KEY,
    ...
);

-- ✅ Dùng NEWSEQUENTIALID() nếu cần GUID
CREATE TABLE Orders (
    OrderId UNIQUEIDENTIFIER DEFAULT NEWSEQUENTIALID() PRIMARY KEY,
    ...
);

3. Non-Clustered Index (NCX)

Khái niệm

Non-Clustered Index là cấu trúc B-tree riêng biệt với bảng dữ liệu:

  • Leaf pages chứa key columns + row locator (clustered key hoặc RID)
  • Mỗi bảng có thể có tối đa 999 non-clustered indexes
-- Tạo non-clustered index
CREATE NONCLUSTERED INDEX IX_Orders_CustomerId 
ON Orders (CustomerId);

-- Với included columns
CREATE NONCLUSTERED INDEX IX_Orders_CustomerId_Include
ON Orders (CustomerId)
INCLUDE (OrderDate, TotalAmount);

-- Composite key
CREATE NONCLUSTERED INDEX IX_OrderDetails_Composite
ON OrderDetails (OrderId, ProductId);

Key Columns vs Included Columns

Leaf Page của NCX:
┌─────────────────────────────────────────────┐
│ Key Column(s): CustomerId                   │
│ Included Columns: OrderDate, TotalAmount    │  ← Chỉ ở leaf level
│ Row Locator: OrderId (clustered key)        │
└─────────────────────────────────────────────┘
  • Key columns: xuất hiện ở tất cả levels của B-tree, dùng cho seeking và sorting
  • Included columns: chỉ ở leaf level, không dùng để seek nhưng tránh key lookup
-- Query này có thể được thỏa mãn hoàn toàn bởi index (covering index)
SELECT CustomerId, OrderDate, TotalAmount  -- Covered!
FROM Orders
WHERE CustomerId = 12345;                  -- Seek trên CustomerId

-- Nếu index không có TotalAmount → key lookup xảy ra
-- Với INCLUDE (TotalAmount) → không cần lookup

4. Index Pages: Page Splits và Fill Factor

Page Structure

SQL Server lưu dữ liệu theo pages 8KB. Mỗi page chứa nhiều rows.

Page Split

Khi INSERT vào giữa một clustered index mà page đã đầy:

  1. SQL Server phải tạo page mới
  2. Di chuyển ~50% dữ liệu sang page mới
  3. Update pointer chain → tốn I/O, gây fragmentation
Trước split:        Sau split:
┌──────────┐        ┌──────────┐    ┌──────────┐
│  1, 2, 3 │  →    │  1, 2    │ →  │  2.5, 3  │
│  (full)  │        │ (50%)    │    │  (50%)   │
└──────────┘        └──────────┘    └──────────┘

Fill Factor

Fill Factor xác định % không gian trống để lại trên mỗi leaf page khi rebuild/create index.

-- Tạo index với fill factor 80% (20% để trống cho insert sau)
CREATE INDEX IX_Orders_OrderDate ON Orders (OrderDate)
WITH (FILLFACTOR = 80);

-- Rebuild với fill factor mới
ALTER INDEX IX_Orders_OrderDate ON Orders
REBUILD WITH (FILLFACTOR = 80);

-- Kiểm tra fill factor của các index
SELECT 
    i.name,
    i.fill_factor
FROM sys.indexes i
WHERE i.object_id = OBJECT_ID('Orders');
Fill FactorDùng khiHệ quả
100 (0)Read-only tablesTốn ít space, nhiều page split nếu có insert
80-90Mixed workloadCân bằng space và performance
60-70Heavy insert/update vào giữaNhiều không gian hơn, ít split hơn

5. Covering Index

Một index được gọi là Covering Index khi nó chứa tất cả các cột mà query cần - không cần quay về bảng gốc để lấy thêm dữ liệu.

-- Query cần: WHERE CustomerId, SELECT OrderId, OrderDate, TotalAmount
SELECT OrderId, OrderDate, TotalAmount
FROM Orders
WHERE CustomerId = 12345
  AND OrderDate >= '2024-01-01';

-- ✅ Covering Index cho query trên
CREATE INDEX IX_Orders_Cover
ON Orders (CustomerId, OrderDate)     -- Key: dùng để seek + filter
INCLUDE (OrderId, TotalAmount);       -- Included: chỉ cần ở SELECT

-- ❌ Non-covering: chỉ có CustomerId
-- → SQL Server phải làm Key Lookup cho mỗi hàng tìm được
CREATE INDEX IX_Orders_CustomerId ON Orders (CustomerId);

Key Lookup vs Covering

-- Kiểm tra key lookup trong execution plan
-- Tìm queries có nhiều key lookups
SELECT TOP 20
    qs.execution_count,
    qs.total_logical_reads,
    SUBSTRING(qt.text, (qs.statement_start_offset/2)+1,
        ((CASE qs.statement_end_offset WHEN -1 THEN DATALENGTH(qt.text)
          ELSE qs.statement_end_offset END - 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_logical_reads DESC;

6. Index Seek vs Index Scan vs Table Scan

Index Seek

SQL Server đi qua B-tree từ root đến leaf để tìm đúng rows. Xảy ra khi:

  • SARGable predicate (Search ARGument ABLE)
  • Predicate match với leading key column của index

SARGable vs Non-SARGable

SARGable là predicate mà SQL Server có thể dùng để tìm kiếm trong index.

-- ✅ SARGable - có thể dùng index seek
WHERE CustomerId = 12345
WHERE OrderDate >= '2024-01-01'
WHERE LastName LIKE 'Nguyen%'     -- leading wildcard không có → SARGable
WHERE Price BETWEEN 100 AND 500

-- ❌ Non-SARGable - buộc index/table scan
WHERE YEAR(OrderDate) = 2024              -- Hàm trên cột
WHERE LEFT(LastName, 3) = 'Ngu'          -- Hàm trên cột
WHERE UPPER(Email) = 'TEST@GMAIL.COM'    -- Hàm trên cột
WHERE LastName LIKE '%Nguyen'             -- Leading wildcard
WHERE Price + 10 > 200                    -- Biểu thức trên cột
WHERE CAST(OrderId AS VARCHAR) = '123'   -- Implicit/explicit cast
-- ✅ Cách viết lại SARGable
-- Thay YEAR(OrderDate) = 2024
WHERE OrderDate >= '2024-01-01' AND OrderDate < '2025-01-01'

-- Thay UPPER(Email) = 'TEST@GMAIL.COM'
-- Giải pháp: dùng collation case-insensitive (mặc định SQL Server)
WHERE Email = 'test@gmail.com'   -- CI collation → tự động case-insensitive

Khi SQL Server chọn Scan thay vì Seek

-- SQL Server chọn scan khi:
-- 1. Ước tính % rows được trả về cao (thường > 5-30%)
-- 2. Predicate không SARGable
-- 3. Không có thống kê tốt
-- 4. Index không phù hợp với query

-- Kiểm tra selectivity
SELECT 
    COUNT(DISTINCT CustomerId) * 1.0 / COUNT(*) AS Selectivity
FROM Orders;
-- Selectivity gần 1 = index seek rất hiệu quả
-- Selectivity gần 0 = có thể scan tốt hơn

7. Missing Index Recommendations

SQL Server tự động thu thập thông tin về các index mà nó “muốn có” trong sys.dm_db_missing_index_* DMVs.

-- Truy vấn missing indexes được khuyến nghị (sắp xếp theo impact)
SELECT TOP 20
    mid.statement AS TableName,
    migs.avg_total_user_cost * migs.avg_user_impact * (migs.user_seeks + migs.user_scans) AS improvement_measure,
    migs.user_seeks,
    migs.user_scans,
    migs.avg_total_user_cost,
    migs.avg_user_impact,
    mid.equality_columns,
    mid.inequality_columns,
    mid.included_columns,
    -- Tạo lệnh CREATE INDEX gợi ý
    'CREATE INDEX IX_' + 
        REPLACE(REPLACE(mid.statement, '[', ''), ']', '') + '_' +
        ISNULL(REPLACE(mid.equality_columns, ', ', '_'), '') +
        ' ON ' + mid.statement + 
        ' (' + ISNULL(mid.equality_columns, '') + 
        CASE WHEN mid.inequality_columns IS NOT NULL 
             THEN CASE WHEN mid.equality_columns IS NOT NULL THEN ', ' ELSE '' END + mid.inequality_columns 
             ELSE '' END + ')' +
        ISNULL(' INCLUDE (' + mid.included_columns + ')', '') AS create_index_statement
FROM sys.dm_db_missing_index_groups mig
INNER JOIN sys.dm_db_missing_index_group_stats migs ON mig.index_group_handle = migs.group_handle
INNER JOIN sys.dm_db_missing_index_details mid ON mig.index_handle = mid.index_handle
WHERE mid.database_id = DB_ID()
ORDER BY improvement_measure DESC;

Cảnh báo: Đừng tạo tất cả suggested indexes mù quáng. Cân nhắc:

  • Có bị overlap với index hiện có không?
  • Có thực sự cần thiết không (improvement_measure đủ cao)?
  • Tổng số indexes không nên quá nhiều → ảnh hưởng write performance

8. Index Maintenance: REBUILD vs REORGANIZE

Kiểm tra Fragmentation

-- Kiểm tra mức độ fragmentation của tất cả index trong database
SELECT 
    t.name AS TableName,
    i.name AS IndexName,
    ips.index_type_desc,
    ips.avg_fragmentation_in_percent,
    ips.page_count
FROM sys.dm_db_index_physical_stats(
    DB_ID(),      -- Database ID (NULL = tất cả DB)
    NULL,         -- Object ID (NULL = tất cả tables)
    NULL,         -- Index ID (NULL = tất cả indexes)
    NULL,         -- Partition number
    'LIMITED'     -- Mode: LIMITED, SAMPLED, DETAILED
) ips
INNER JOIN sys.tables t ON ips.object_id = t.object_id
INNER JOIN sys.indexes i ON ips.object_id = i.object_id AND ips.index_id = i.index_id
WHERE ips.page_count > 100   -- Chỉ xem index đủ lớn
ORDER BY ips.avg_fragmentation_in_percent DESC;

REORGANIZE vs REBUILD

REORGANIZEREBUILD
Fragmentation< 30%> 30%
LockOnline (không block)Offline hoặc ONLINE option
Log usageÍt hơnNhiều hơn
StatisticsKhông updateUpdate (FULLSCAN)
Fill factorDùng fill factor hiện tạiCó thể chỉ định mới
-- REORGANIZE (online, ít lock)
ALTER INDEX IX_Orders_CustomerId ON Orders REORGANIZE;

-- REBUILD (offline, defragment hoàn toàn)
ALTER INDEX IX_Orders_CustomerId ON Orders REBUILD;

-- REBUILD ONLINE (SQL Server Enterprise)
ALTER INDEX IX_Orders_CustomerId ON Orders REBUILD WITH (ONLINE = ON);

-- REBUILD tất cả indexes của bảng
ALTER INDEX ALL ON Orders REBUILD WITH (FILLFACTOR = 85);

-- Script auto-maintain dựa trên fragmentation
DECLARE @fragmentation FLOAT;

SELECT @fragmentation = avg_fragmentation_in_percent
FROM sys.dm_db_index_physical_stats(DB_ID(), OBJECT_ID('Orders'), 
    INDEXPROPERTY(OBJECT_ID('Orders'), 'IX_Orders_CustomerId', 'IndexID'), NULL, 'LIMITED');

IF @fragmentation > 30
    ALTER INDEX IX_Orders_CustomerId ON Orders REBUILD;
ELSE IF @fragmentation > 10
    ALTER INDEX IX_Orders_CustomerId ON Orders REORGANIZE;

9. sys.indexes - Querying Index Metadata

-- Xem tất cả indexes của một bảng
SELECT 
    i.name AS IndexName,
    i.type_desc AS IndexType,
    i.is_unique,
    i.is_primary_key,
    i.fill_factor,
    -- Key columns
    STRING_AGG(
        CASE WHEN ic.is_included_column = 0 THEN c.name END, ', '
    ) WITHIN GROUP (ORDER BY ic.key_ordinal) AS KeyColumns,
    -- Included columns
    STRING_AGG(
        CASE WHEN ic.is_included_column = 1 THEN c.name END, ', '
    ) AS IncludedColumns
FROM sys.indexes i
INNER JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
WHERE i.object_id = OBJECT_ID('Orders')
GROUP BY i.name, i.type_desc, i.is_unique, i.is_primary_key, i.fill_factor
ORDER BY i.type_desc, i.name;

-- Xem index usage statistics
SELECT 
    i.name AS IndexName,
    i.type_desc,
    ius.user_seeks,
    ius.user_scans,
    ius.user_lookups,
    ius.user_updates,
    ius.last_user_seek,
    ius.last_user_scan
FROM sys.indexes i
LEFT JOIN sys.dm_db_index_usage_stats ius 
    ON i.object_id = ius.object_id 
    AND i.index_id = ius.index_id 
    AND ius.database_id = DB_ID()
WHERE i.object_id = OBJECT_ID('Orders')
ORDER BY (ISNULL(ius.user_seeks, 0) + ISNULL(ius.user_scans, 0)) DESC;

-- Tìm các index không được dùng (candidates for removal)
SELECT 
    t.name AS TableName,
    i.name AS IndexName,
    ius.user_seeks,
    ius.user_scans,
    ius.user_updates,
    ius.last_user_seek
FROM sys.indexes i
INNER JOIN sys.tables t ON i.object_id = t.object_id
LEFT JOIN sys.dm_db_index_usage_stats ius 
    ON i.object_id = ius.object_id 
    AND i.index_id = ius.index_id 
    AND ius.database_id = DB_ID()
WHERE i.type_desc = 'NONCLUSTERED'
  AND i.is_primary_key = 0
  AND i.is_unique_constraint = 0
  AND (ius.user_seeks IS NULL OR ius.user_seeks = 0)   -- Chưa từng được seek
  AND (ius.user_scans IS NULL OR ius.user_scans = 0)   -- Chưa từng được scan
ORDER BY ISNULL(ius.user_updates, 0) DESC;  -- Vẫn tốn chi phí maintain

Index Nâng Cao


1. Filtered Index (Index có điều kiện WHERE)

Filtered Index là non-clustered index được xây dựng chỉ trên một tập con hàng thỏa mãn điều kiện WHERE. Kết quả: index nhỏ hơn, hiệu quả hơn, ít maintenance hơn.

-- Ví dụ 1: Chỉ index các đơn hàng chưa hoàn thành
CREATE NONCLUSTERED INDEX IX_Orders_Pending
ON Orders (OrderDate, CustomerId)
INCLUDE (TotalAmount)
WHERE Status = 'Pending';   -- Filtered predicate

-- Ví dụ 2: Soft delete - chỉ index active records
CREATE NONCLUSTERED INDEX IX_Products_Active
ON Products (CategoryId, Price)
WHERE IsDeleted = 0;

-- Ví dụ 3: Sparse column - chỉ index hàng có giá trị (không NULL)
CREATE NONCLUSTERED INDEX IX_Employees_Email
ON Employees (Email)
WHERE Email IS NOT NULL;

Khi nào dùng Filtered Index

Use caseVí dụ
Sparse dataCột có nhiều NULL (chỉ index hàng có giá trị)
Soft deletesWHERE IsDeleted = 0
Status filteringWHERE Status = 'Active' hoặc 'Pending'
Recent dataWHERE CreatedDate >= '2024-01-01'
Partial uniquenessUnique trong subset (vd: unique email cho active users)
-- Filtered Unique Index: Email phải unique giữa các active users
CREATE UNIQUE NONCLUSTERED INDEX UIX_Users_Email_Active
ON Users (Email)
WHERE IsDeleted = 0;

-- Filtered index CHỈ được dùng khi query có predicate tương thích
-- Ví dụ: query này SẼ dùng IX_Orders_Pending
SELECT OrderId, OrderDate, CustomerId, TotalAmount
FROM Orders
WHERE Status = 'Pending'          -- ✅ Match filtered predicate
  AND OrderDate >= '2024-01-01';

-- Query này KHÔNG dùng IX_Orders_Pending (không có WHERE Status = 'Pending')
SELECT OrderId FROM Orders WHERE CustomerId = 123;

2. Columnstore Index

Columnstore Index lưu dữ liệu theo cột thay vì theo hàng - tối ưu cho analytical queries (OLAP) với aggregation trên nhiều hàng.

Clustered Columnstore Index (CCI)

Thay thế hoàn toàn table heap/clustered row-store.

-- Tạo bảng với Clustered Columnstore Index
CREATE TABLE FactSales (
    SaleId BIGINT,
    ProductId INT,
    CustomerId INT,
    SaleDate DATE,
    Quantity INT,
    UnitPrice DECIMAL(18,2),
    TotalAmount DECIMAL(18,2)
);

CREATE CLUSTERED COLUMNSTORE INDEX CCI_FactSales ON FactSales;

-- Hoặc khi tạo bảng (SQL Server 2022+)
-- WITH (CLUSTERED COLUMNSTORE INDEX)

Non-Clustered Columnstore Index (NCCI)

Thêm analytical capability vào bảng OLTP mà không thay đổi row-store.

-- Thêm NCCI vào bảng Orders OLTP để hỗ trợ reporting queries
CREATE NONCLUSTERED COLUMNSTORE INDEX NCCI_Orders_Analytics
ON Orders (OrderDate, CustomerId, ProductId, TotalAmount, Status);

Cơ chế hoạt động

Columnstore Storage (theo cột):
ProductId column:  [1, 1, 2, 3, 3, 3, 5, 5, ...]  → Compress tốt (giá trị lặp lại)
SaleDate column:   [2024-01-01, 2024-01-01, ...]   → RLE compression
TotalAmount column:[99.99, 149.00, 49.99, ...]

Row-store (theo hàng):
Row 1: [1, 1, 2024-01-01, 99.99, ...]
Row 2: [1, 1, 2024-01-01, 149.00, ...]
Đặc điểmRow-storeColumnstore
Lưu trữTheo hàngTheo cột
CompressionThấp (~1x)Cao (5-10x điển hình)
Point queriesTốtKém
Aggregation trên nhiều hàngChậmRất nhanh (batch mode)
Write performanceTốtKém hơn (delta store)
Phù hợpOLTPOLAP, Data Warehouse

Delta Store & Tuple Mover

Khi INSERT vào Columnstore:
1. Rows mới → Delta Store (row-store B-tree, nhỏ)
2. Khi Delta Store đủ lớn (~1M rows)
3. Tuple Mover (background) compress và chuyển vào Column Segments
4. Deleted rows → Delete Bitmap (không xóa ngay)
-- Kiểm tra trạng thái Delta Store và Deleted rows
SELECT 
    i.name,
    rg.state_desc,
    rg.total_rows,
    rg.deleted_rows,
    rg.size_in_bytes
FROM sys.column_store_row_groups rg
INNER JOIN sys.indexes i ON rg.object_id = i.object_id AND rg.index_id = i.index_id
WHERE rg.object_id = OBJECT_ID('FactSales');

Batch Mode Execution

Columnstore kích hoạt Batch Mode: xử lý 64-900 hàng cùng lúc thay vì từng hàng. Kết hợp với SIMD CPU instructions → nhanh hơn 5-100x cho analytical queries.

-- Query analytical hưởng lợi từ CCI + Batch Mode
SELECT 
    ProductId,
    YEAR(SaleDate) AS SaleYear,
    SUM(TotalAmount) AS Revenue,
    COUNT(*) AS TxnCount
FROM FactSales
GROUP BY ProductId, YEAR(SaleDate)
ORDER BY Revenue DESC;

3. Full-Text Index

Full-Text Index cho phép tìm kiếm ngôn ngữ tự nhiên trong văn bản - không thể làm được với LIKE hoặc standard index.

-- Bước 1: Tạo Full-Text Catalog
CREATE FULLTEXT CATALOG ftCatalog AS DEFAULT;

-- Bước 2: Tạo Full-Text Index
CREATE FULLTEXT INDEX ON Products (ProductName, Description)
KEY INDEX PK_Products  -- Phải có unique index
ON ftCatalog
WITH STOPLIST = SYSTEM;  -- Dùng stopword list mặc định

-- CONTAINS: tìm kiếm chính xác
SELECT ProductId, ProductName
FROM Products
WHERE CONTAINS(ProductName, 'laptop');

-- CONTAINS với nhiều từ
WHERE CONTAINS(Description, '"high performance" AND "gaming"')

-- FREETEXT: tìm kiếm ngôn ngữ tự nhiên (stemming, synonyms)
WHERE FREETEXT(Description, 'fast computer processor')

-- CONTAINSTABLE: trả về ranking
SELECT p.ProductId, p.ProductName, ft.RANK
FROM Products p
INNER JOIN CONTAINSTABLE(Products, Description, 'gaming laptop') ft
    ON p.ProductId = ft.[KEY]
ORDER BY ft.RANK DESC;

LIKE vs Full-Text

LIKEFull-Text
Pattern matching'%keyword%' (leading wildcard = scan)Indexed word lookup
Hiệu suấtChậm với %...%Nhanh
Stemming (run/runs/running)Không
Proximity searchKhôngCó (NEAR)
RankingKhôngCó (RANK)
SetupKhông cầnCần tạo catalog + index

4. XML Index

Cho bảng có cột kiểu XML - tăng tốc XQuery và XPath expressions.

-- Bảng với cột XML
CREATE TABLE Orders (
    OrderId INT PRIMARY KEY,
    OrderDetails XML
);

-- Primary XML Index: bắt buộc tạo trước
CREATE PRIMARY XML INDEX PXML_Orders_Details
ON Orders (OrderDetails);

-- Secondary XML Indexes (tùy loại query)
CREATE XML INDEX SXML_Orders_PATH   ON Orders (OrderDetails) USING XML INDEX PXML_Orders_Details FOR PATH;
CREATE XML INDEX SXML_Orders_VALUE  ON Orders (OrderDetails) USING XML INDEX PXML_Orders_Details FOR VALUE;
CREATE XML INDEX SXML_Orders_PROPERTY ON Orders (OrderDetails) USING XML INDEX PXML_Orders_Details FOR PROPERTY;
Secondary XML IndexTối ưu cho
PATH/path expressions
VALUE//tag = 'value' lookups
PROPERTYĐọc nhiều properties từ cùng node

5. Spatial Index

Cho cột kiểu GEOMETRY hoặc GEOGRAPHY.

-- Bảng với cột Geography
CREATE TABLE Locations (
    LocationId INT PRIMARY KEY,
    Name NVARCHAR(100),
    GeoPoint GEOGRAPHY
);

INSERT INTO Locations VALUES 
    (1, 'Hanoi', geography::STPointFromText('POINT(105.8412 21.0278)', 4326)),
    (2, 'Saigon', geography::STPointFromText('POINT(106.6297 10.8231)', 4326));

CREATE SPATIAL INDEX SIX_Locations_GeoPoint
ON Locations (GeoPoint)
USING GEOGRAPHY_GRID
WITH (GRIDS = (MEDIUM, MEDIUM, MEDIUM, MEDIUM), CELLS_PER_OBJECT = 16);

-- Query tìm locations trong vòng 10km từ một điểm
DECLARE @center GEOGRAPHY = geography::STPointFromText('POINT(105.8412 21.0278)', 4326);

SELECT Name, GeoPoint.STDistance(@center) AS DistanceMeters
FROM Locations
WHERE GeoPoint.STDistance(@center) <= 10000
ORDER BY DistanceMeters;

6. Unique Index

Đảm bảo không có giá trị trùng lặp trong cột/nhóm cột được index.

-- Unique index đơn giản
CREATE UNIQUE NONCLUSTERED INDEX UIX_Users_Email
ON Users (Email);

-- Unique index cho composite key
CREATE UNIQUE NONCLUSTERED INDEX UIX_Products_Category_Code
ON Products (CategoryId, ProductCode);

-- Xử lý NULL: Unique index cho phép nhiều NULL
-- (NULL != NULL trong SQL logic)
INSERT INTO Users (Email) VALUES (NULL);
INSERT INTO Users (Email) VALUES (NULL);  -- ✅ Thành công! NULL không vi phạm unique

-- Nếu muốn NULL cũng unique → dùng Filtered Index
CREATE UNIQUE NONCLUSTERED INDEX UIX_Users_Email_NotNull
ON Users (Email)
WHERE Email IS NOT NULL;

7. Index with INCLUDE Columns (Covering Index)

-- Composite key với INCLUDE: cân bằng between seek efficiency và covering
CREATE NONCLUSTERED INDEX IX_Orders_Customer_Date
ON Orders (CustomerId, OrderDate)   -- Key: dùng để seek, có thứ tự
INCLUDE (OrderId, Status, TotalAmount);  -- Chỉ ở leaf, không ảnh hưởng B-tree height

-- Tại sao không đưa tất cả vào key?
-- Key columns: xuất hiện ở mọi levels → làm B-tree rộng hơn → nhiều page hơn
-- INCLUDE: chỉ ở leaf level → không làm to B-tree → space hiệu quả hơn

Quyết định đưa cột vào Key hay INCLUDE

Điều kiệnĐưa vào KeyĐưa vào INCLUDE
Cần WHERE/JOIN/ORDER BY
Chỉ cần SELECT
Muốn có unique constraint❌ (không thể)

8. Composite Index: Thứ tự cột quan trọng

Column order trong composite index ảnh hưởng trực tiếp đến query nào có thể dùng index.

-- Index: (CustomerId, OrderDate, Status)
CREATE INDEX IX_Orders ON Orders (CustomerId, OrderDate, Status);

-- ✅ Có thể dùng index (leading columns match)
WHERE CustomerId = 100
WHERE CustomerId = 100 AND OrderDate = '2024-01-01'
WHERE CustomerId = 100 AND OrderDate BETWEEN '2024-01-01' AND '2024-12-31'
WHERE CustomerId = 100 AND OrderDate = '2024-01-01' AND Status = 'Active'

-- ⚠️ Partial use (chỉ dùng CustomerId cho seek, Status filter sau)
WHERE CustomerId = 100 AND Status = 'Active'  -- Bỏ qua OrderDate

-- ❌ KHÔNG thể dùng index seek (không có leading column)
WHERE OrderDate = '2024-01-01'
WHERE Status = 'Active'

Nguyên tắc chọn thứ tự cột

  1. Equality first: Cột dùng = đặt trước
  2. Range last: Cột dùng >, <, BETWEEN đặt sau
  3. Selectivity: Cột có nhiều distinct values → seek hiệu quả hơn
  4. ORDER BY / GROUP BY: Nếu order match với index → tránh Sort operator
-- Query: WHERE Status = 'Active' AND CreatedDate > '2024-01-01'
-- Status có 3 giá trị, CreatedDate rất nhiều giá trị

-- Option A: (Status, CreatedDate) → seek trên Status=Active, range scan CreatedDate
-- Option B: (CreatedDate, Status) → range scan CreatedDate, filter Status
-- → Option A thường tốt hơn vì seek trên equality trước

9. Index Intersection vs Index Covering

Index Intersection

SQL Server dùng nhiều indexes cho cùng một query và merge kết quả (AND/OR).

-- Có 2 indexes riêng
CREATE INDEX IX_Orders_CustomerId ON Orders (CustomerId);
CREATE INDEX IX_Orders_Status ON Orders (Status);

-- Query sau có thể trigger Index Intersection
WHERE CustomerId = 100 AND Status = 'Active';
-- SQL Server: seek IX_Orders_CustomerId → get OrderIds
--             seek IX_Orders_Status → get OrderIds
--             INTERSECT hai tập → key lookup cho rows trong cả hai

Index Covering vs Index Intersection

Covering IndexIndex Intersection
Cần1 index với tất cả cột cầnNhiều indexes
Hiệu suấtThường tốt hơnCó thêm cost merge
FlexibilityIndex cụ thể cho queryIndexes có thể dùng nhiều query
Khi nào optimizer chọnKhi covering index tồn tạiKhi không có covering index nhưng có nhiều partial indexes

10. In-Memory OLTP Indexes (Memory-Optimized Tables)

In-Memory OLTP tables (Hekaton) dùng các loại index khác hoàn toàn.

-- Tạo memory-optimized table
CREATE TABLE dbo.SessionCache (
    SessionId NVARCHAR(100) NOT NULL,
    UserId INT NOT NULL,
    Data NVARCHAR(MAX),
    CreatedAt DATETIME2 NOT NULL,
    CONSTRAINT PK_SessionCache PRIMARY KEY NONCLUSTERED HASH (SessionId)
        WITH (BUCKET_COUNT = 1000000)
) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_ONLY);

Hash Index (In-Memory)

-- Hash Index: O(1) point lookup, không hỗ trợ range
CONSTRAINT PK_Session PRIMARY KEY NONCLUSTERED HASH (SessionId)
    WITH (BUCKET_COUNT = 1000000);  -- Phải ước tính số distinct values

-- ✅ Tốt cho: = equality lookup
-- ❌ Không dùng được cho: range queries, ORDER BY

Range Index (Nonclustered, In-Memory)

-- Nonclustered range index: Bw-tree (lock-free B-tree)
-- Hỗ trợ range queries, ORDER BY
INDEX IX_Session_UserId NONCLUSTERED (UserId, CreatedAt)
Hash IndexRange Index (Nonclustered)
Point lookupO(1)O(log N)
Range scan❌ Không tốt✅ Tốt
ORDER BY❌ Không✅ Có
Phù hợpCache, session lookupsQueries có range/order

11. Hypothetical Indexes (Index giả định)

SQL Server Database Engine Tuning Advisor (DTA) và một số tools dùng hypothetical indexes để test xem index có cải thiện query không mà không cần tạo thật.

-- Tạo hypothetical index (WITH STATISTICS_ONLY)
-- Tạo statistics nhưng KHÔNG tạo cấu trúc index thật
CREATE NONCLUSTERED INDEX IX_Orders_Test
ON Orders (CustomerId, OrderDate)
WITH STATISTICS_ONLY = -1;   -- Hypothetical

-- Bật chế độ cho phép optimizer dùng hypothetical indexes
DBCC AUTOPILOT(0, DB_ID(), OBJECT_ID('Orders'), ...);

-- Thực tế: Dùng Database Engine Tuning Advisor
-- 1. Capture workload (Profiler Trace hoặc Extended Events)
-- 2. Chạy DTA với workload
-- 3. DTA generates recommendations
-- 4. Review và apply các recommendations cần thiết

Thực tế: Hypothetical indexes hiếm khi dùng trực tiếp trong T-SQL. Chủ yếu là internal mechanism của DTA. Lập trình viên thường test bằng cách tạo index thật trên môi trường dev/staging.

Query Optimization - Tối Ưu Truy Vấn


1. Query Processing Pipeline

Mỗi khi SQL Server nhận một query, nó đi qua pipeline 4 bước:

SQL Text
   ↓
[1. Parser]          → Kiểm tra syntax, tạo Parse Tree
   ↓
[2. Algebrizer/Bind] → Resolve tên objects/columns, kiểm tra semantics
   ↓
[3. Query Optimizer] → Tạo Execution Plan tối ưu (cost-based)
   ↓
[4. Execution Engine]→ Thực thi plan, trả kết quả

Chi tiết từng bước

-- Bước 1: Parser - kiểm tra syntax
SELECCT * FROM Orders;   -- ❌ Syntax error tại Parser

-- Bước 2: Algebrizer - resolve objects
SELECT * FROM NonExistentTable;   -- ❌ Object not found tại Algebrizer

-- Bước 3: Optimizer - tạo plan
-- Optimizer thử nhiều alternatives, chọn plan có estimated cost thấp nhất
-- Không phải luôn chọn plan THỰC TẾ nhanh nhất (chỉ dựa vào statistics)

-- Bước 4: Execution
-- Thực thi plan, có thể khác với estimate nếu statistics không chính xác

2. Cost-Based Optimizer

SQL Server’s Query Optimizer (QO) là cost-based: nó tạo nhiều plan alternatives và chọn plan có estimated cost thấp nhất.

Cost dựa trên gì?

  • Statistics: phân phối dữ liệu của cột (histogram)
  • Cardinality estimates: ước tính số hàng mỗi operator trả về
  • I/O cost: số page reads ước tính
  • CPU cost: computational cost
-- Xem statistics của một column
DBCC SHOW_STATISTICS ('Orders', 'IX_Orders_CustomerId');
-- Kết quả: Header (stats), Density Vector, Histogram (phân phối giá trị)

-- Cập nhật statistics
UPDATE STATISTICS Orders IX_Orders_CustomerId WITH FULLSCAN;
UPDATE STATISTICS Orders;  -- Tất cả stats cho bảng

-- Xem khi statistics được update lần cuối
SELECT 
    s.name AS StatName,
    sp.last_updated,
    sp.rows,
    sp.rows_sampled,
    sp.modification_counter
FROM sys.stats s
CROSS APPLY sys.dm_db_stats_properties(s.object_id, s.stats_id) sp
WHERE s.object_id = OBJECT_ID('Orders');

Estimated vs Actual Rows

-- Bật actual execution plan (Ctrl+M trong SSMS)
-- Xem: Estimated Rows vs Actual Rows trong mỗi operator
-- Nếu chênh lệch lớn → statistics không chính xác → plan không tốt

-- Ví dụ: Optimizer ước tính 10 rows nhưng thực tế 1,000,000 rows
-- → Optimizer chọn Nested Loops (tốt cho nhỏ) nhưng thực tế nên dùng Hash Join

3. SARGable Predicates

SARGable (Search ARGument ABLE) là predicate mà SQL Server có thể dùng để thực hiện index seek thay vì scan.

Nguyên tắc

Predicate không SARGable khi: cột indexed nằm bên trong một function hoặc biểu thức.

-- ❌ Non-SARGable: hàm trên cột
WHERE YEAR(OrderDate) = 2024
WHERE MONTH(OrderDate) = 6
WHERE UPPER(LastName) = 'NGUYEN'
WHERE LEN(PhoneNumber) = 10
WHERE SUBSTRING(ProductCode, 1, 3) = 'SKU'

-- ✅ Viết lại SARGable
WHERE OrderDate >= '2024-01-01' AND OrderDate < '2025-01-01'
WHERE OrderDate >= '2024-06-01' AND OrderDate < '2024-07-01'
WHERE LastName = 'Nguyen'   -- CI collation → không cần UPPER
WHERE PhoneNumber LIKE '__________'   -- Nếu phải dùng LIKE
WHERE ProductCode LIKE 'SKU%'

-- ❌ Non-SARGable: biểu thức trên cột
WHERE Price * 1.1 > 100
WHERE Age + 5 = 30
WHERE OrderId + 1 = @Param

-- ✅ Viết lại: di chuyển biểu thức sang phía giá trị
WHERE Price > 100 / 1.1        -- ≈ 90.9
WHERE Age = 30 - 5             -- = 25
WHERE OrderId = @Param - 1

-- ❌ Non-SARGable: implicit/explicit conversion
WHERE CAST(OrderId AS VARCHAR) = '1234'
WHERE CONVERT(NVARCHAR, OrderDate, 103) = '01/01/2024'

-- ✅ Giữ đúng kiểu dữ liệu
WHERE OrderId = 1234           -- INT so sánh INT
WHERE OrderDate = '2024-01-01' -- Implicit convert từ string sang DATE là OK

4. Common Anti-Patterns (Lỗi phổ biến)

4.1 Function trong WHERE trên cột indexed

-- ❌ Buộc table scan
SELECT * FROM Orders WHERE YEAR(OrderDate) = 2024;

-- ✅ Index seek
SELECT * FROM Orders 
WHERE OrderDate >= '2024-01-01' AND OrderDate < '2025-01-01';

4.2 LIKE với leading wildcard

-- ❌ Table/Index scan - không thể seek khi bắt đầu bằng %
SELECT * FROM Products WHERE ProductName LIKE '%phone%';

-- ✅ Tốt hơn: trailing wildcard có thể seek
SELECT * FROM Products WHERE ProductName LIKE 'Samsung%';

-- ✅ Nếu cần full-text search: dùng Full-Text Index + CONTAINS
SELECT * FROM Products WHERE CONTAINS(ProductName, 'phone');

4.3 Implicit Conversion

-- Bảng: CustomerCode NVARCHAR(20) (Unicode)
-- ❌ Parameter @code là VARCHAR → implicit conversion, index scan
DECLARE @code VARCHAR(20) = 'CUST001';
SELECT * FROM Customers WHERE CustomerCode = @code;
-- SQL Server phải convert tất cả CustomerCode sang VARCHAR để so sánh

-- ✅ Dùng đúng kiểu dữ liệu
DECLARE @code NVARCHAR(20) = N'CUST001';
SELECT * FROM Customers WHERE CustomerCode = @code;

-- Kiểm tra implicit conversion trong execution plan:
-- Tìm operator có "CONVERT_IMPLICIT" trong predicate

4.4 SELECT *

-- ❌ SELECT * - kéo về nhiều cột không cần thiết
SELECT * FROM Orders WHERE CustomerId = 100;

-- ✅ Chỉ lấy cột cần
SELECT OrderId, OrderDate, TotalAmount FROM Orders WHERE CustomerId = 100;
-- Lợi ích:
-- 1. Giảm network bandwidth
-- 2. Có thể dùng covering index (tránh key lookup)
-- 3. Tránh break code khi schema thay đổi

4.5 OR thay vì UNION ALL (đôi khi)

-- ❌ OR có thể không dùng được multiple indexes hiệu quả
SELECT * FROM Orders 
WHERE CustomerId = 100 OR Status = 'Urgent';

-- ✅ UNION ALL thường hiệu quả hơn
SELECT * FROM Orders WHERE CustomerId = 100
UNION ALL
SELECT * FROM Orders WHERE Status = 'Urgent' AND CustomerId <> 100;

4.6 NOT IN với NULL

-- ❌ Nguy hiểm: nếu BlacklistIds có NULL → query trả về rỗng
SELECT * FROM Orders WHERE CustomerId NOT IN (SELECT CustomerId FROM Blacklist);

-- ✅ An toàn
SELECT * FROM Orders o
WHERE NOT EXISTS (SELECT 1 FROM Blacklist b WHERE b.CustomerId = o.CustomerId);

5. JOIN Algorithms

SQL Server có 3 thuật toán join, optimizer chọn dựa trên kích thước bảng, indexes và statistics.

Nested Loops Join

For each row in Outer:
    Seek/Scan Inner table for matching rows
-- Nested Loops tốt khi:
-- 1. Outer table nhỏ (ít rows sau filter)
-- 2. Inner table có index trên join key
-- 3. Join selectivity cao (ít rows match)

-- Optimizer thường chọn Nested Loops cho:
SELECT c.Name, o.OrderDate
FROM Customers c                      -- Outer (nhỏ, vd: 1 khách)
INNER JOIN Orders o ON o.CustomerId = c.CustomerId  -- Inner (có index)
WHERE c.CustomerId = 12345;

Hash Join

Phase 1 (Build): Hash mọi hàng của Build Input vào hash table
Phase 2 (Probe): Với mỗi hàng Probe Input, lookup trong hash table
-- Hash Join tốt khi:
-- 1. Cả hai inputs lớn, không có index phù hợp
-- 2. Không yêu cầu sorted input
-- 3. Ước tính nhiều rows match

-- ⚠️ Khi hash table không vừa memory → spill sang TempDB (Hash Spill)
-- Xuất hiện trong execution plan là dấu hiệu cần index hoặc tăng memory

-- Xem Hash Spill warnings
SELECT * FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) qp
WHERE CAST(qp.query_plan AS NVARCHAR(MAX)) LIKE '%SpillToTempDb%';

Merge Join

Cả hai inputs phải được SORT theo join key
Đọc song song, merge như merge sort
-- Merge Join tốt khi:
-- 1. Cả hai inputs đã sorted (từ index hoặc Sort operator)
-- 2. JOIN dạng equality (equi-join)
-- 3. Cả hai inputs tương đương lớn

-- Merge Join rất hiệu quả nếu đã có sorted index:
SELECT o.OrderId, od.ProductId
FROM Orders o                          -- Clustered index trên OrderId → sorted
INNER JOIN OrderDetails od ON o.OrderId = od.OrderId  -- Index trên OrderId → sorted
-- → Optimizer thường chọn Merge Join ở đây

So sánh

Nested LoopsHash JoinMerge Join
Yêu cầuIndex trên innerMemory cho hash tableSorted inputs
Tốt nhấtOuter nhỏ + inner indexedLarge unsorted inputsLarge sorted inputs
MemoryThấpCao (có thể spill)Thấp (nếu đã sorted)
ParallelismHạn chếTốtTốt

6. Query Hints

-- OPTION (RECOMPILE): Tạo plan mới mỗi lần, dùng actual parameter values
-- Hữu ích khi parameter sniffing gây vấn đề
SELECT * FROM Orders WHERE CustomerId = @Id
OPTION (RECOMPILE);

-- MAXDOP: Giới hạn số CPU dùng cho query
SELECT * FROM BigTable
OPTION (MAXDOP 4);  -- Tối đa 4 cores

SELECT * FROM SmallQuery
OPTION (MAXDOP 1);  -- Không dùng parallelism

-- OPTIMIZE FOR: Hint cho optimizer dùng giá trị giả định
SELECT * FROM Orders WHERE CustomerId = @Id
OPTION (OPTIMIZE FOR (@Id = 1000));   -- Optimize như thể @Id = 1000

-- OPTIMIZE FOR UNKNOWN: Không dùng cached value, dùng average statistics
SELECT * FROM Orders WHERE CustomerId = @Id
OPTION (OPTIMIZE FOR (@Id UNKNOWN));

-- USE PLAN: Ép dùng execution plan XML cụ thể (advanced)
SELECT * FROM Orders
OPTION (USE PLAN N'<ShowPlanXML.../>');

-- LOOP JOIN / HASH JOIN / MERGE JOIN: Ép thuật toán join
SELECT * FROM Orders o
INNER HASH JOIN Customers c ON o.CustomerId = c.CustomerId
OPTION (HASH JOIN);

-- FORCE ORDER: Ép optimizer join theo thứ tự viết trong query
SELECT * FROM A
INNER JOIN B ON A.Id = B.AId
INNER JOIN C ON B.Id = C.BId
OPTION (FORCE ORDER);

-- NO_PERFORMANCE_SPOOL: Tắt spool optimization
-- DISABLE_OPTIMIZER_ROWGOAL: Tắt row goal optimization
-- ENABLE_PARALLEL_PLAN_PREFERENCE: Ưu tiên plan song song

7. Plan Cache & Parameter Sniffing

Plan Cache

SQL Server cache execution plans để tránh compile lại mỗi lần.

-- Xem plans trong cache
SELECT 
    qs.execution_count,
    qs.total_logical_reads,
    qs.total_elapsed_time / 1000 AS total_ms,
    SUBSTRING(qt.text, 1, 300) AS query_text,
    qp.query_plan
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) qp
ORDER BY qs.total_logical_reads DESC;

-- Xóa tất cả plan cache (chỉ dùng dev/troubleshooting!)
DBCC FREEPROCCACHE;

-- Xóa plan cụ thể
DBCC FREEPROCCACHE (@plan_handle);

Parameter Sniffing

Khi stored procedure compiles lần đầu, optimizer dùng giá trị parameter lúc đó để tạo plan. Plan này có thể tốt cho giá trị đó nhưng tệ cho giá trị khác.

-- Stored procedure
CREATE PROCEDURE GetOrdersByCustomer @CustomerId INT
AS
BEGIN
    SELECT OrderId, OrderDate, TotalAmount
    FROM Orders
    WHERE CustomerId = @CustomerId;
END;

-- Lần đầu gọi với CustomerId = 1 (có 1,000,000 orders → optimizer dùng Table Scan)
EXEC GetOrdersByCustomer 1;  -- Plan: Table Scan (tốt cho nhiều rows)

-- Lần sau gọi với CustomerId = 99999 (có 5 orders)
EXEC GetOrdersByCustomer 99999;  -- ❌ Dùng lại plan cũ (Table Scan cho 5 rows → rất chậm!)

Giải pháp Parameter Sniffing

-- Giải pháp 1: OPTION (RECOMPILE) - compile lại mỗi lần
CREATE PROCEDURE GetOrdersByCustomer @CustomerId INT
AS
BEGIN
    SELECT OrderId, OrderDate, TotalAmount
    FROM Orders
    WHERE CustomerId = @CustomerId
    OPTION (RECOMPILE);  -- Mất benefit của plan caching
END;

-- Giải pháp 2: OPTIMIZE FOR UNKNOWN
WHERE CustomerId = @CustomerId
OPTION (OPTIMIZE FOR (@CustomerId UNKNOWN));

-- Giải pháp 3: Local variable trick (không được sniff)
CREATE PROCEDURE GetOrdersByCustomer @CustomerId INT
AS
BEGIN
    DECLARE @LocalId INT = @CustomerId;  -- Optimizer không thể sniff local var
    SELECT * FROM Orders WHERE CustomerId = @LocalId;
END;

-- Giải pháp 4: Multiple procedures cho các scenarios khác nhau
IF @CustomerId IN (1, 2, 3)  -- Known "high volume" customers
    EXEC GetOrdersByCustomer_HighVolume @CustomerId
ELSE
    EXEC GetOrdersByCustomer_Normal @CustomerId

8. Recompilation

Khi nào plan bị recompile?

-- 1. Schema thay đổi (ALTER TABLE, CREATE INDEX, etc.)
-- 2. Statistics thay đổi (sau threshold = 20% of rows + 500 rows modified)
-- 3. SET options thay đổi trong session
-- 4. Database thay đổi (sp_recompile)
-- 5. OPTION (RECOMPILE) hint

-- Buộc recompile tất cả procedures liên quan đến table
EXEC sp_recompile 'Orders';

-- Xem recompilation events với Extended Events hoặc Profiler
-- Event: sql_statement_recompile (Extended Events)
-- Event Class: SP:Recompile (Profiler)

Xem Queries với nhiều recompiles

SELECT 
    qs.plan_generation_num,  -- Số lần plan được regenerate
    qs.execution_count,
    SUBSTRING(qt.text, 1, 200) AS query_text
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
WHERE qs.plan_generation_num > 10
ORDER BY qs.plan_generation_num DESC;

9. TempDB Usage

Temp Tables vs Table Variables

-- Temp Table (#)
CREATE TABLE #TempOrders (
    OrderId INT,
    CustomerId INT,
    TotalAmount DECIMAL(18,2)
);
-- Trong TempDB, có statistics, có thể có indexes
-- Scope: session (hoặc nested scope)

INSERT INTO #TempOrders SELECT OrderId, CustomerId, TotalAmount FROM Orders WHERE ...;

-- Table Variable (@)
DECLARE @TempOrders TABLE (
    OrderId INT,
    CustomerId INT,
    TotalAmount DECIMAL(18,2)
);
-- Không có statistics (optimizer assume 1 row!)
-- Scope: batch
-- Không commit/rollback riêng
Temp TableTable Variable
StatisticsCó (accurate cardinality)Không (luôn ước tính 1 row)
IndexesTạo đượcChỉ inline indexes
Recompile on populateCó thể (nếu stats thay đổi)Không
TransactionTham gia transaction bên ngoàiKhông bị rollback
ScopeSession / nested scopesBatch
TempDB usageCó (nhưng thường ít hơn)
Khi nào dùng> 1000 rows, cần joins phức tạp< 100 rows, đơn giản

Worktables trong TempDB

SQL Server tự tạo worktables trong TempDB cho:

  • Hash Join spill (khi memory không đủ)
  • Sort spill
  • Spool operations
  • Recursive CTE
-- Monitor TempDB usage
SELECT 
    r.session_id,
    r.total_elapsed_time,
    tdb.internal_objects_alloc_page_count AS temp_pages_allocated,
    SUBSTRING(qt.text, 1, 200) AS query_text
FROM sys.dm_exec_requests r
INNER JOIN sys.dm_db_task_space_usage tdb ON r.session_id = tdb.session_id
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) qt
WHERE tdb.internal_objects_alloc_page_count > 0
ORDER BY tdb.internal_objects_alloc_page_count DESC;

10. Parallelism (Xử lý song song)

Khi nào query đi parallel?

SQL Server sẽ cân nhắc parallel plan khi:

  1. Estimated cost > cost threshold for parallelism (mặc định: 5)
  2. MAXDOP không = 1
  3. Query đủ phức tạp để benefit từ parallelism
-- Xem cấu hình parallelism
SELECT name, value_in_use
FROM sys.configurations 
WHERE name IN ('max degree of parallelism', 'cost threshold for parallelism');

-- Đổi cost threshold (khuyến nghị: 25-50 cho OLTP)
EXEC sp_configure 'cost threshold for parallelism', 50;
RECONFIGURE;

-- Đổi MAXDOP toàn server
EXEC sp_configure 'max degree of parallelism', 8;
RECONFIGURE;

MAXDOP Recommendations

WorkloadMAXDOP
OLTP heavy1 (tránh parallel overhead)
Mixed workload4-8
OLAP/DWH0 (tất cả cores) hoặc số cores per NUMA node
Reporting queriesHIGH (tối đa cores)
-- Query-level MAXDOP
SELECT * FROM BigTable OPTION (MAXDOP 8);

-- Database-level MAXDOP (SQL Server 2016+)
ALTER DATABASE SCOPED CONFIGURATION SET MAXDOP = 4;

Vấn đề với Parallelism

-- CXPACKET wait: worker threads chờ nhau trong parallel query
-- Xem top waits
SELECT TOP 10
    wait_type,
    waiting_tasks_count,
    wait_time_ms,
    max_wait_time_ms
FROM sys.dm_os_wait_stats
WHERE wait_type NOT IN ('SLEEP_TASK', 'BROKER_TO_FLUSH', 'WAITFOR', 'CLR_AUTO_EVENT')
ORDER BY wait_time_ms DESC;

-- CXPACKET cao có thể chỉ ra:
-- 1. Parallelism không cần thiết (tăng cost threshold)
-- 2. Skewed data distribution (uneven work distribution)
-- 3. Worker thread chờ I/O

11. DMVs for Performance Analysis

sys.dm_exec_query_stats

-- Top 20 queries tốn nhiều logical reads nhất
SELECT TOP 20
    qs.execution_count,
    qs.total_logical_reads,
    qs.total_logical_reads / qs.execution_count AS avg_logical_reads,
    qs.total_elapsed_time / 1000000 AS total_seconds,
    qs.total_elapsed_time / qs.execution_count / 1000 AS avg_ms,
    SUBSTRING(qt.text, (qs.statement_start_offset/2)+1, 
        ((CASE qs.statement_end_offset 
            WHEN -1 THEN DATALENGTH(qt.text) 
            ELSE qs.statement_end_offset END 
         - 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_logical_reads DESC;

sys.dm_exec_requests

-- Xem các query đang chạy hiện tại
SELECT 
    r.session_id,
    r.status,
    r.blocking_session_id,
    r.wait_type,
    r.wait_time / 1000 AS wait_seconds,
    r.total_elapsed_time / 1000 AS elapsed_seconds,
    r.cpu_time,
    r.logical_reads,
    DB_NAME(r.database_id) AS database_name,
    SUBSTRING(qt.text, (r.statement_start_offset/2)+1, 200) AS current_statement
FROM sys.dm_exec_requests r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) qt
WHERE r.session_id != @@SPID   -- Loại bỏ session hiện tại
ORDER BY r.total_elapsed_time DESC;

sys.dm_os_wait_stats

-- Phân tích wait statistics (reset sau mỗi lần restart hoặc DBCC SQLPERF)
SELECT TOP 20
    wait_type,
    waiting_tasks_count,
    wait_time_ms,
    max_wait_time_ms,
    signal_wait_time_ms,
    wait_time_ms - signal_wait_time_ms AS resource_wait_ms
FROM sys.dm_os_wait_stats
WHERE wait_type NOT IN (
    'SLEEP_TASK', 'BROKER_TO_FLUSH', 'BROKER_EVENTHANDLER',
    'CHECKPOINT_QUEUE', 'DBMIRROR_EVENTS_QUEUE', 'SQLTRACE_BUFFER_FLUSH',
    'CLR_AUTO_EVENT', 'DISPATCHER_QUEUE_SEMAPHORE', 'FT_IFTS_SCHEDULER_IDLE_WAIT',
    'HADR_FILESTREAM_IOMGR_IOCOMPLETION', 'HADR_WORK_QUEUE', 'LAZYWRITER_SLEEP',
    'LOGMGR_QUEUE', 'ONDEMAND_TASK_QUEUE', 'REQUEST_FOR_DEADLOCK_SEARCH',
    'RESOURCE_QUEUE', 'SERVER_IDLE_CHECK', 'SLEEP_DBSTARTUP', 'SLEEP_DCOMSTARTUP',
    'SLEEP_MASTERDBREADY', 'SLEEP_MASTERMDREADY', 'SLEEP_MASTERUPGRADED',
    'SLEEP_MSDBSTARTUP', 'SLEEP_SYSTEMTASK', 'SLEEP_TEMPDBSTARTUP',
    'SNI_HTTP_ACCEPT', 'SP_SERVER_DIAGNOSTICS_SLEEP', 'SQLTRACE_BUFFER_FLUSH',
    'WAIT_XTP_OFFLINE_CKPT_NEW_LOG', 'XE_DISPATCHER_WAIT', 'XE_TIMER_EVENT'
)
ORDER BY wait_time_ms DESC;

Common Wait Types

Wait TypeNguyên nhânGiải pháp
PAGEIOLATCH_SH/EXI/O chờ đọc/ghi pageThêm RAM (buffer pool), optimize query, SSD
LCK_M_XLock contentionOptimize transactions, shorter TX scope
CXPACKETParallel query syncGiảm MAXDOP, tăng cost threshold
ASYNC_NETWORK_IOClient đọc kết quả chậmGiảm result set, pagination
SOS_SCHEDULER_YIELDCPU pressureThêm CPU, optimize CPU-heavy queries
WRITELOGTransaction log I/OFaster disk cho log, tránh nhiều small transactions

12. Extended Events vs SQL Trace

SQL Trace (Legacy - không dùng trên production mới)

-- SQL Trace (Profiler) - deprecated
-- Không dùng trên SQL Server 2019+ cho workloads mới

Extended Events (XE) - Modern approach

-- Tạo Extended Events session để capture slow queries (> 5 giây)
CREATE EVENT SESSION [SlowQueries] ON SERVER
ADD EVENT sqlserver.sql_statement_completed (
    ACTION (
        sqlserver.sql_text,
        sqlserver.client_hostname,
        sqlserver.username,
        sqlserver.database_name
    )
    WHERE (duration > 5000000)  -- > 5 giây (đơn vị: microseconds)
),
ADD EVENT sqlserver.rpc_completed (
    ACTION (sqlserver.sql_text)
    WHERE (duration > 5000000)
)
ADD TARGET package0.ring_buffer (SET max_memory = 51200)  -- 50MB buffer
WITH (MAX_DISPATCH_LATENCY = 5 SECONDS);

-- Bắt đầu session
ALTER EVENT SESSION [SlowQueries] ON SERVER STATE = START;

-- Đọc kết quả từ ring_buffer
SELECT  
    xdr.value('@name', 'nvarchar(50)') AS event_name,
    xdr.value('(action[@name="sql_text"]/value)[1]', 'nvarchar(max)') AS sql_text,
    xdr.value('(data[@name="duration"]/value)[1]', 'bigint') / 1000000.0 AS duration_sec,
    xdr.value('(data[@name="logical_reads"]/value)[1]', 'bigint') AS logical_reads,
    xdr.value('@timestamp', 'datetime2') AS event_time
FROM (
    SELECT CAST(target_data AS XML) AS target_data
    FROM sys.dm_xe_session_targets xst
    INNER JOIN sys.dm_xe_sessions xs ON xst.event_session_address = xs.address
    WHERE xs.name = 'SlowQueries' AND xst.target_name = 'ring_buffer'
) t
CROSS APPLY t.target_data.nodes('RingBufferTarget/event') AS xTable(xdr)
ORDER BY xdr.value('@timestamp', 'datetime2') DESC;

-- Dừng và xóa session
ALTER EVENT SESSION [SlowQueries] ON SERVER STATE = STOP;
DROP EVENT SESSION [SlowQueries] ON SERVER;

Query Store (SQL Server 2016+)

Query Store lưu lịch sử plans và performance - không cần XE session để track regressions.

-- Bật Query Store
ALTER DATABASE YourDB SET QUERY_STORE = ON;
ALTER DATABASE YourDB SET QUERY_STORE (
    OPERATION_MODE = READ_WRITE,
    CLEANUP_POLICY = (STALE_QUERY_THRESHOLD_DAYS = 30),
    DATA_FLUSH_INTERVAL_SECONDS = 60,
    MAX_SIZE_MB = 1024,
    QUERY_CAPTURE_MODE = AUTO   -- Capture queries vượt ngưỡng
);

-- Top queries theo CPU trong Query Store
SELECT TOP 20
    q.query_id,
    qt.query_sql_text,
    rs.avg_cpu_time,
    rs.avg_logical_io_reads,
    rs.avg_duration,
    rs.count_executions
FROM sys.query_store_query q
INNER JOIN sys.query_store_query_text qt ON q.query_text_id = qt.query_text_id
INNER JOIN sys.query_store_plan p ON q.query_id = p.query_id
INNER JOIN sys.query_store_runtime_stats rs ON p.plan_id = rs.plan_id
ORDER BY rs.avg_cpu_time DESC;

-- Force a specific plan (plan regression fix)
EXEC sp_query_store_force_plan @query_id = 1, @plan_id = 2;

So sánh XE vs Query Store vs Profiler

SQL Trace/ProfilerExtended EventsQuery Store
OverheadCaoThấpRất thấp
Real-timeDelayed
HistoryKhông (file only)Không (ring buffer)Có (persistent)
Plan trackingKhôngCó (với action)Có (automatic)
Plan forcingKhôngKhông
Khuyến nghịKhông dùng mớiTroubleshootingDefault cho mọi DB

Statistics & Cardinality Estimation

Thống kê (Statistics) là gì?

Statistics là metadata mà SQL Server Query Optimizer sử dụng để ước tính số lượng rows (cardinality) sẽ được trả về bởi một query operation. Optimizer dùng thông tin này để chọn execution plan tối ưu nhất.

Thành phần của Statistics

Thành phầnMô tả
HeaderTên table, tên index/column, thời gian update
Density VectorThống kê tính selectivity của column combinations
HistogramPhân phối giá trị của leading column

Histogram

Histogram chia dữ liệu thành tối đa 200 steps (SQL Server 2016+), mỗi step chứa:

  • RANGE_HI_KEY: Giá trị cao nhất trong step
  • RANGE_ROWS: Số rows có giá trị trong range (không bao gồm boundary)
  • EQ_ROWS: Số rows có giá trị bằng RANGE_HI_KEY
  • DISTINCT_RANGE_ROWS: Số distinct values trong range
  • AVG_RANGE_ROWS: Average rows per distinct value trong range

Density Vector

Density = 1 / number_of_distinct_values. Density Vector chứa density cho mọi prefix combination của index columns.

-- Xem density vector và histogram
DBCC SHOW_STATISTICS ('Sales.Orders', 'IX_Orders_CustomerID');
-- Hoặc chi tiết hơn
DBCC SHOW_STATISTICS ('Sales.Orders', 'IX_Orders_CustomerID') WITH HISTOGRAM;
DBCC SHOW_STATISTICS ('Sales.Orders', 'IX_Orders_CustomerID') WITH DENSITY_VECTOR;
DBCC SHOW_STATISTICS ('Sales.Orders', 'IX_Orders_CustomerID') WITH STAT_HEADER;

Auto-Create và Auto-Update Statistics

AUTO_CREATE_STATISTICS

SQL Server tự động tạo statistics khi optimizer cần thông tin cho một column chưa có statistics.

-- Kiểm tra setting của database
SELECT name, is_auto_create_stats_on, is_auto_update_stats_on,
       is_auto_update_stats_async_on
FROM sys.databases
WHERE name = 'YourDatabase';

-- Bật Auto Create Statistics
ALTER DATABASE YourDatabase SET AUTO_CREATE_STATISTICS ON;

-- Bật Async Auto Update
ALTER DATABASE YourDatabase SET AUTO_UPDATE_STATISTICS_ASYNC ON;

AUTO_UPDATE_STATISTICS

SQL Server tự động update statistics khi dữ liệu thay đổi vượt ngưỡng:

  • SQL Server 2016 trở về trước: 20% rows thay đổi (+ 500 rows cho table nhỏ)
  • SQL Server 2016+ với compatibility level 130+: Dynamic threshold (dùng sqrt(1000 * table_rows))
-- Bật Auto Update Statistics
ALTER DATABASE YourDatabase SET AUTO_UPDATE_STATISTICS ON;

-- Async update: query không cần chờ statistics update xong
ALTER DATABASE YourDatabase SET AUTO_UPDATE_STATISTICS_ASYNC ON;

Lưu ý: AUTO_UPDATE_STATISTICS_ASYNC ON giúp tránh blocking nhưng plan hiện tại có thể dùng statistics cũ cho query đó. Plan mới sẽ được dùng cho lần chạy tiếp theo.


Xem và Phân Tích Statistics

sys.statistics và sys.stats_columns

-- Xem tất cả statistics trên một table
SELECT 
    s.name AS stats_name,
    s.auto_created,
    s.user_created,
    s.has_filter,
    s.filter_definition,
    s.stats_id,
    STATS_DATE(s.object_id, s.stats_id) AS last_updated,
    sp.rows,
    sp.rows_sampled,
    sp.steps,
    sp.unfiltered_rows,
    sp.modification_counter
FROM sys.statistics s
CROSS APPLY sys.dm_db_stats_properties(s.object_id, s.stats_id) sp
WHERE s.object_id = OBJECT_ID('Sales.Orders')
ORDER BY s.name;

-- Xem các columns trong statistics
SELECT 
    s.name AS stats_name,
    sc.stats_column_id,
    c.name AS column_name
FROM sys.statistics s
JOIN sys.stats_columns sc ON s.object_id = sc.object_id AND s.stats_id = sc.stats_id
JOIN sys.columns c ON sc.object_id = c.object_id AND sc.column_id = c.column_id
WHERE s.object_id = OBJECT_ID('Sales.Orders');

Kiểm tra Statistics Staleness

-- Tìm statistics cũ (chưa update > 7 ngày và có nhiều modifications)
SELECT 
    OBJECT_NAME(s.object_id) AS table_name,
    s.name AS stats_name,
    STATS_DATE(s.object_id, s.stats_id) AS last_updated,
    sp.modification_counter,
    sp.rows,
    CAST(100.0 * sp.modification_counter / NULLIF(sp.rows, 0) AS DECIMAL(5,2)) AS pct_modified
FROM sys.statistics s
CROSS APPLY sys.dm_db_stats_properties(s.object_id, s.stats_id) sp
WHERE sp.modification_counter > 0
    AND STATS_DATE(s.object_id, s.stats_id) < DATEADD(DAY, -7, GETDATE())
ORDER BY sp.modification_counter DESC;

Cập Nhật Statistics

UPDATE STATISTICS Options

-- Update tất cả statistics trên table với full scan
UPDATE STATISTICS Sales.Orders WITH FULLSCAN;

-- Update một statistics cụ thể
UPDATE STATISTICS Sales.Orders IX_Orders_CustomerID WITH FULLSCAN;

-- Dùng sample thay vì full scan (nhanh hơn, ít chính xác hơn)
UPDATE STATISTICS Sales.Orders WITH SAMPLE 30 PERCENT;
UPDATE STATISTICS Sales.Orders WITH SAMPLE 10000 ROWS;

-- Update tất cả statistics trong database
EXEC sp_updatestats; -- Chỉ update nếu có thay đổi

-- Update tất cả với full scan (chậm hơn nhưng chính xác nhất)
EXEC sp_msforeachtable 'UPDATE STATISTICS ? WITH FULLSCAN';

-- Dùng ROWCOUNT/PAGECOUNT để override (KHÔNG khuyến nghị cho production)
UPDATE STATISTICS Sales.Orders WITH ROWCOUNT = 5000000, PAGECOUNT = 100000;

UPDATE STATISTICS vs sp_updatestats

UPDATE STATISTICSsp_updatestats
Phạm viMột table/statisticsToàn bộ database
Điều kiệnLuôn updateChỉ update nếu có modification
ControlTùy chọn FULLSCAN/SAMPLEDùng default sample rate
Sử dụngMaintenance scheduleQuick refresh

Statistics Update Thresholds

Old Threshold (trước SQL 2016)

Threshold = 500 + 20% of table rows

  • Table 100 rows: update sau 520 changes
  • Table 1,000,000 rows: update sau 200,500 changes
  • Vấn đề: Table lớn rất khó trigger auto-update → outdated statistics

Dynamic Threshold (SQL 2016+ với Compat Level 130+)

Threshold = sqrt(1000 * table_rows)

  • Table 1,000,000 rows: update sau ~31,623 changes (thay vì 200,500)
  • Kích hoạt bằng cách set compatibility level ≥ 130
-- Kiểm tra compatibility level
SELECT name, compatibility_level FROM sys.databases WHERE name = DB_NAME();

-- Nâng compatibility level để dùng dynamic threshold
ALTER DATABASE YourDatabase SET COMPATIBILITY_LEVEL = 150; -- SQL 2019

-- Hoặc dùng trace flag 2371 (SQL 2008-2014 để enable dynamic threshold sớm)
DBCC TRACEON (2371, -1);

Trace Flag 2389 và 2390 — Ascending Key Problem

Vấn đề Ascending Key

Khi một column (thường là identity hoặc datetime) có giá trị mới liên tục được INSERT vào vượt ngoài histogram, optimizer sẽ ước tính 0 hoặc 1 row → chọn plan sai.

-- Ví dụ: OrderDate là ascending key
-- Histogram chỉ biết đến ngày hôm qua
-- Query hôm nay: optimizer estimate 1 row → chọn Nested Loops
-- Thực tế: 50,000 rows → cần Hash Join

SELECT * FROM Sales.Orders WHERE OrderDate >= '2024-01-01';

Giải pháp với Trace Flag

-- TF 2389: SQL Server detect ascending key statistic và mark histogram là "ascending"
-- TF 2390: Tương tự nhưng dùng cho unknown pattern (không chỉ ascending)
DBCC TRACEON (2389, -1);
DBCC TRACEON (2390, -1);

-- SQL Server 2016+ với CE 120+: Dùng Average Row Count thay vì estimate 1 row
-- Không cần trace flags nếu dùng CE mới

Giải pháp thực tế

-- 1. Update statistics thường xuyên hơn (sau batch insert lớn)
UPDATE STATISTICS Sales.Orders IX_Orders_OrderDate WITH FULLSCAN;

-- 2. Tạo Filtered Statistics cho range mới
CREATE STATISTICS ST_Orders_RecentDates 
ON Sales.Orders (OrderDate)
WHERE OrderDate >= '2024-01-01'
WITH FULLSCAN;

-- 3. Dùng OPTION (RECOMPILE) để force fresh estimate
SELECT * FROM Sales.Orders 
WHERE OrderDate >= @StartDate
OPTION (RECOMPILE);

Cardinality Estimation (CE)

CE70 vs CE120+

CE70 (Legacy)CE120+ (New)
IntroducedSQL Server 7.0SQL Server 2014
Compat Level≤ 110≥ 120
Multi-predicateIndependence assumptionExponential backoff
JOIN estimationSimple formulaMore sophisticated
Ascending keysEstimate 1 rowBetter estimate
-- Kiểm tra CE model đang dùng
SELECT compatibility_level FROM sys.databases WHERE name = DB_NAME();

-- Force CE70 (Legacy) cho một query cụ thể
SELECT * FROM Sales.Orders
WHERE CustomerID = 1 AND StatusID = 2
OPTION (USE HINT ('FORCE_LEGACY_CARDINALITY_ESTIMATION'));

-- Force CE120
SELECT * FROM Sales.Orders
OPTION (USE HINT ('ENABLE_QUERY_OPTIMIZER_HOTFIXES'));

-- Force CE ở database level
ALTER DATABASE SCOPED CONFIGURATION SET LEGACY_CARDINALITY_ESTIMATION = ON;

Kiểm tra và Xác định CE Issues

-- So sánh estimated vs actual rows trong execution plan
-- Dùng STATISTICS XML để xem chi tiết
SET STATISTICS XML ON;
SELECT * FROM Sales.Orders WHERE CustomerID = 100;
SET STATISTICS XML OFF;

-- Dùng Query Store để tìm regressed queries
SELECT 
    qsq.query_id,
    qsq.query_hash,
    qsp.plan_id,
    qsrs.avg_logical_io_reads,
    qsrs.avg_cpu_time,
    TRY_CAST(qsp.query_plan AS XML) AS query_plan
FROM sys.query_store_query qsq
JOIN sys.query_store_plan qsp ON qsq.query_id = qsp.query_id
JOIN sys.query_store_runtime_stats qsrs ON qsp.plan_id = qsrs.plan_id
ORDER BY qsrs.avg_logical_io_reads DESC;

Cardinality Issues: Estimated vs Actual Row Count

Cách Identify CE Problems

  1. Execution Plan: Nhìn vào “Estimated Number of Rows” vs “Actual Number of Rows”
  2. Nếu ratio > 10x: Có thể có cardinality estimation problem
  3. Triệu chứng: Nested Loops thay vì Hash Join, hoặc ngược lại
-- Query để tìm queries có cardinality mismatch lớn trong Query Store
SELECT TOP 20
    qsq.query_id,
    qsp.plan_id,
    qsrs.count_executions,
    qsrs.avg_logical_io_reads,
    (qsrs.avg_logical_io_reads * qsrs.count_executions) AS total_io,
    qsrs.avg_cpu_time / 1000.0 AS avg_cpu_ms,
    SUBSTRING(qsqt.query_sql_text, 1, 200) AS query_text
FROM sys.query_store_query qsq
JOIN sys.query_store_query_text qsqt ON qsq.query_text_id = qsqt.query_text_id
JOIN sys.query_store_plan qsp ON qsq.query_id = qsp.query_id
JOIN sys.query_store_runtime_stats qsrs ON qsp.plan_id = qsrs.plan_id
WHERE qsrs.avg_logical_io_reads > 10000
ORDER BY total_io DESC;

Filtered Statistics

Filtered Statistics cho phép tạo statistics chỉ trên một subset của data, giúp optimizer có thông tin chính xác hơn cho filtered queries.

-- Tạo filtered statistics cho orders của năm hiện tại
CREATE STATISTICS ST_Orders_Current_Year
ON Sales.Orders (CustomerID, OrderDate)
WHERE YEAR(OrderDate) = YEAR(GETDATE())
WITH FULLSCAN;

-- Tạo filtered statistics cho active products
CREATE STATISTICS ST_Products_Active
ON Products (CategoryID, Price)
WHERE IsActive = 1
WITH FULLSCAN;

-- Xem filtered statistics
SELECT 
    s.name,
    s.has_filter,
    s.filter_definition,
    STATS_DATE(s.object_id, s.stats_id) AS last_updated
FROM sys.statistics s
WHERE s.object_id = OBJECT_ID('Sales.Orders')
    AND s.has_filter = 1;

Multi-Column Statistics

Multi-column statistics cung cấp thông tin về correlation (sự tương quan) giữa các columns.

-- Tạo multi-column statistics
CREATE STATISTICS ST_Orders_Customer_Status
ON Sales.Orders (CustomerID, StatusID, OrderDate)
WITH FULLSCAN;

-- Xem density vector của multi-column statistics
-- Density càng thấp → column combination càng selective
DBCC SHOW_STATISTICS ('Sales.Orders', 'ST_Orders_Customer_Status') WITH DENSITY_VECTOR;

Density và Correlation

  • Density = 1 / distinct_values: Density thấp = selective column
  • Multi-column density giúp optimizer biết nếu kết hợp columns giảm result set bao nhiêu
  • Nếu optimizer không có multi-column stats, nó assume column independence → ước tính sai

Statistics cho Non-Indexed Columns

SQL Server có thể auto-create statistics cho non-indexed columns (nếu AUTO_CREATE_STATISTICS ON). Bạn cũng có thể tạo thủ công:

-- Tạo statistics thủ công cho column không có index
CREATE STATISTICS ST_Orders_Notes
ON Sales.Orders (Notes)
WITH FULLSCAN;

-- Xem tất cả auto-created statistics (không phải từ index)
SELECT 
    OBJECT_NAME(s.object_id) AS table_name,
    s.name AS stats_name,
    s.auto_created,
    STATS_DATE(s.object_id, s.stats_id) AS last_updated
FROM sys.statistics s
WHERE s.auto_created = 1
    AND s.object_id > 100  -- Exclude system objects
ORDER BY OBJECT_NAME(s.object_id), s.name;

-- Xóa auto-created statistics không cần thiết
DROP STATISTICS Sales.Orders._WA_Sys_00000001_1ED998B2;

Statistics cho tempdb và In-Memory OLTP

tempdb Statistics

-- Statistics trong tempdb được tạo cho temp tables và table variables
-- Temp tables: auto-create and auto-update statistics (theo database setting)
-- Table variables: KHÔNG có statistics → optimizer assume 1 row

CREATE TABLE #TempOrders (
    OrderID INT,
    CustomerID INT,
    OrderDate DATE
);

-- Tạo statistics thủ công cho temp table
CREATE STATISTICS ST_TempOrders_Customer ON #TempOrders (CustomerID);

-- Verify
DBCC SHOW_STATISTICS ('#TempOrders', 'ST_TempOrders_Customer');

In-Memory OLTP Statistics

-- Memory-Optimized tables có statistics riêng
-- Xem statistics cho memory-optimized tables
SELECT 
    OBJECT_NAME(s.object_id) AS table_name,
    s.name AS stats_name,
    STATS_DATE(s.object_id, s.stats_id) AS last_updated
FROM sys.statistics s
JOIN sys.tables t ON s.object_id = t.object_id
WHERE t.is_memory_optimized = 1;

-- Update statistics cho memory-optimized table
UPDATE STATISTICS dbo.MemOptOrders WITH FULLSCAN;

Q&A theo Cấp Độ

Junior Level

Q: Statistics là gì và tại sao quan trọng?

A: Statistics là tập metadata mô tả phân phối dữ liệu trong một column hoặc set of columns. Query Optimizer dùng statistics để ước tính số rows sẽ được trả về (cardinality), từ đó chọn execution plan phù hợp (index scan vs seek, nested loops vs hash join). Statistics không chính xác → plan kém hiệu quả → query chậm.

Q: AUTO_CREATE_STATISTICS và AUTO_UPDATE_STATISTICS là gì?

A: AUTO_CREATE_STATISTICS ON cho phép SQL Server tự tạo statistics cho columns chưa có khi cần cho optimization. AUTO_UPDATE_STATISTICS ON cho phép SQL Server tự update statistics khi dữ liệu thay đổi vượt ngưỡng. Cả hai nên được bật trong hầu hết môi trường production.

Q: Làm sao xem statistics của một table?

A:

-- Xem danh sách statistics
SELECT name, STATS_DATE(object_id, stats_id) AS last_updated
FROM sys.statistics
WHERE object_id = OBJECT_ID('YourTable');

-- Xem chi tiết histogram
DBCC SHOW_STATISTICS ('YourTable', 'IndexOrStatsName');

Mid Level

Q: Giải thích vấn đề Ascending Key và cách giải quyết?

A: Vấn đề xảy ra khi column có giá trị mới luôn lớn hơn maximumvalue trong histogram (identity column, datetime). Optimizer không biết về các giá trị mới này và ước tính 0-1 row, dẫn đến plan sai. Giải pháp:

  1. Update statistics thường xuyên hơn (sau batch insert lớn)
  2. Dùng Trace Flag 2389/2390 (SQL 2014 trở về)
  3. Nâng Compatibility Level ≥ 130 (CE 120 xử lý tốt hơn)
  4. Dùng Filtered Statistics cho data mới
  5. OPTION (RECOMPILE) cho ad-hoc queries

Q: Filtered Statistics là gì và khi nào nên dùng?

A: Filtered Statistics là statistics được tạo trên một subset của data (với WHERE clause). Dùng khi:

  • Query thường xuyên filter trên một giá trị cụ thể hoặc range
  • Auto statistics không đủ chính xác vì data distribution không đều
  • Partition pruning cần statistics cho từng partition range
CREATE STATISTICS ST_Active_Customers
ON Customers (Region, CustomerType)
WHERE IsActive = 1
WITH FULLSCAN;

Q: Sự khác biệt giữa UPDATE STATISTICS WITH FULLSCAN và default sample rate?

A: Default sample rate tự động chọn dựa trên kích thước table (thường 20-30% cho table lớn). WITH FULLSCAN scan toàn bộ data → statistics chính xác nhất nhưng tốn I/O và thời gian nhất. Với table rất lớn (>100M rows), FULLSCAN có thể ảnh hưởng production. Trade-off: accuracy vs performance cost. Recommendation: dùng FULLSCAN trong maintenance window, sample rate cho frequent updates.


Senior Level

Q: Giải thích sự khác biệt giữa CE70 và CE120 và khi nào nên rollback về CE70?

A:

  • CE70: Model cũ từ SQL 7.0, assume column independence cho multi-predicate (multiply selectivities). Đơn giản nhưng có thể overestimate hoặc underestimate.
  • CE120: Model mới SQL 2014+, dùng exponential backoff cho multi-predicate (giảm dần impact của mỗi predicate sau), xử lý ascending keys tốt hơn, JOIN estimation sophistication hơn.

Khi nào rollback CE70: Sau khi nâng compatibility level, nếu một số query bị regression (chạy chậm hơn với CE120), có thể rollback:

  • Database level: ALTER DATABASE SCOPED CONFIGURATION SET LEGACY_CARDINALITY_ESTIMATION = ON
  • Query level: OPTION (USE HINT('FORCE_LEGACY_CARDINALITY_ESTIMATION'))
  • Nên dùng Query Store để track regression trước khi quyết định

Q: Trong một hệ thống với table partitioning, làm sao manage statistics hiệu quả?

A:

  1. Incremental Statistics (SQL 2014+): Update statistics chỉ cho partition có data thay đổi, không cần scan toàn bộ table.
-- Tạo partitioned table với incremental statistics
CREATE STATISTICS ST_Orders_OrderDate ON Sales.Orders (OrderDate)
WITH FULLSCAN, INCREMENTAL = ON;

-- Update statistics chỉ cho partition 5
UPDATE STATISTICS Sales.Orders ST_Orders_OrderDate
WITH RESAMPLE ON PARTITIONS (5);
  1. Per-partition filtered statistics: Tạo filtered stats cho từng partition range để optimizer có thông tin chi tiết hơn về từng partition.

  2. Monitor modification counter per partition: Dùng sys.dm_db_incremental_stats_properties thay vì sys.dm_db_stats_properties cho incremental stats.

Q: Làm sao diagnose và fix một slow query do cardinality estimation problem?

A: Step 1 - Identify: Bật Actual Execution Plan, so sánh “Estimated Rows” vs “Actual Rows”. Nếu ratio > 10x, có CE problem.

Step 2 - Root cause:

  • Statistics cũ? → STATS_DATE()modification_counter
  • Ascending key? → Xem histogram, giá trị query có nằm ngoài RANGE_HI_KEY max không?
  • Complex predicate? → Optimizer dùng correlation hay independence assumption?
  • Parameter sniffing? → DBCC FREEPROCCACHE rồi chạy lại với actual parameter

Step 3 - Fix options (theo mức độ invasive):

  1. UPDATE STATISTICS ... WITH FULLSCAN (least invasive)
  2. Tạo Filtered Statistics hoặc Multi-column Statistics
  3. OPTION (RECOMPILE) cho stored procedure với parameter sniffing
  4. Query Store: Force good plan với sp_query_store_force_plan
  5. Nâng/hạ Compatibility Level
  6. Hint: OPTION (USE HINT ('ASSUME_JOIN_CONTAINMENT')) etc.

Execution Plans

Execution plans are the roadmap that SQL Server uses to retrieve data. Understanding how to read and analyze execution plans is crucial for performance tuning.

Topics

Overview

An execution plan shows how SQL Server will execute a query, including:

  • Which indexes are used
  • Join methods
  • Scan types (table scan vs index scan/seek)
  • Sort operations
  • And various other operators

Why Execution Plans Matter

  • Performance Tuning: Identify bottlenecks in queries
  • Index Analysis: Understand if indexes are being used effectively
  • Query Optimization: See why a query is slow and how to improve it
  • Cardinality Estimates: Understand row count predictions

How to View Execution Plans

Using SSMS (SQL Server Management Studio)

  1. Enable “Include Actual Execution Plan” (Ctrl + M)
  2. Run your query
  3. View the Execution Plan tab

Using T-SQL

SET STATISTICS XML ON;
-- Your query here
SET STATISTICS XML OFF;

Common Operators

OperatorDescription
Table ScanReads all rows from a table
Index ScanReads all entries from an index
Index SeekUses index to find specific rows
Clustered Index ScanScans entire clustered index (entire table)
Nested LoopsJoins using nested iteration
Hash JoinJoins using hash table
Merge JoinJoins using sorted inputs
Compute ScalarCalculates new values from existing data
SortOrders data
FilterFilters rows based on condition

Further Reading

For detailed information about specific operators, see the topics in this section.

Compute Scalar

Compute Scalar is an operator that computes new values from existing data within a row. It does not read data from disk, only processes data in memory.

Common Use Cases

  • Calculations: Quantity * Price AS TotalAmount
  • Functions: UPPER(FirstName), YEAR(OrderDate)
  • Type Casting: CAST(OrderId AS VARCHAR)
  • Concatenation: FirstName + ' ' + LastName

Parameters Explained

1. Physical Operation / Logical Operation

Physical Operation: Compute Scalar
Logical Operation: Compute Scalar
  • Physical Operation: How SQL Server executes (compute scalar)
  • Logical Operation: Logical meaning (also compute scalar)

In this case, both are the same since the operator has no special variants.

2. Estimated Execution Mode

Estimated Execution Mode: Row
  • Row: Processes row-by-row
  • Batch: Processes in batch mode (usually with columnstore indexes)

This is a simple operator, always running in row mode.

3. Estimated Operator Cost

Estimated Operator Cost: 0.001982 (2%)

Most important!

  • 0.001982: Absolute cost of this operator
  • (2%): Relative cost compared to the entire query (2% of total cost)

Meaning:

  • This operator only takes 2% of the total query cost → NOT A BOTTLENECK
  • If this number is > 30-50%, you should consider optimization

4. Estimated I/O Cost

Estimated I/O Cost: 0
  • This is the I/O cost (reading from disk)
  • 0 means this operator does not read data from disk, only processes data already in memory

✅ This is good - no I/O here.

5. Estimated Subtree Cost

Estimated Subtree Cost: 0.117937
  • Total cost of the entire subtree from this operator downward
  • Includes the current operator and all child operators

Meaning:

  • This is the accumulated cost
  • In the plan, you’ll see this value increase as you go up the tree toward the root

6. Estimated CPU Cost

Estimated CPU Cost: 0.001982
  • CPU cost specifically for this operator
  • In this case, it equals the Estimated Operator Cost (0.001982) because there is no I/O

Comparison with I/O Cost:

CPU Cost = 0.001982
I/O Cost = 0
Total = 0.001982

7. Estimated Number of Executions

Estimated Number of Executions: 1
  • How many times this operator is executed
  • 1 is normal for top-level operators
  • If > 1, the operator may be in a loop (nested loop join) and could be a performance issue

8. Estimated Number of Rows

Estimated Number of Rows: 19820
  • SQL Server estimates this operator will process 19,820 rows
  • This is the cardinality estimate - very important for plan selection

Comparison with Actual Rows:

  • If Actual Rows differs significantly from Estimated Rows → statistics issue

9. Estimated Row Size

Estimated Row Size: 56 B
  • Each row takes approximately 56 bytes in memory
  • Used to estimate memory grant

Calculation:

56 bytes × 19,820 rows ≈ 1.1 MB (memory required)

10. Node ID

Node ID: 0
  • ID of the operator in the execution plan
  • Used for referencing, debugging, or searching in the XML plan

Overall Analysis

Based on these parameters, here’s the assessment:

✅ Good Points:

MetricValueAssessment
I/O Cost0No disk reads
CPU Cost0.001982Very small
% Cost2%Not a bottleneck
Executions1No loop repetition

⚠️ Needs Checking:

  • Estimated Rows = 19,820: Compare with Actual Rows in the actual plan (SET STATISTICS XML ON)
  • If Actual Rows >> 19,820 (e.g., 200,000) → Statistics outdated → Run UPDATE STATISTICS

Real-world Examples: When Compute Scalar Becomes an Issue?

Bad Case (bottleneck):

| Operator          | Cost    | Rows   | Executions |
|-------------------|---------|--------|------------|
| Compute Scalar    | 85%     | 1M     | 1000       |

Problem:

  • Takes 85% CPU, runs 1000 times (in a loop)
  • Consider: move calculation to application, use computed column, or optimize join logic

Your Case (good):

| Operator          | Cost    | Rows   | Executions |
|-------------------|---------|--------|------------|
| Compute Scalar    | 2%      | 19,820 | 1          |

No optimization needed, focus on other operators with higher cost.

How to View Actual vs Estimated in SSMS

To see actual metrics:

-- Enable Actual Execution Plan (Ctrl + M)
SET STATISTICS XML ON; -- Or use SSMS UI

-- Run your query
SELECT 
    OrderId,
    Quantity * Price AS TotalAmount, -- Compute Scalar will appear
    UPPER(CustomerName) AS UpperName
FROM Orders
WHERE OrderDate > '2024-01-01';

Then in the Execution Plan, hover over the operator to see both Estimated and Actual:

Actual Number of Rows: 19,820    (if it matches Estimated → good)
Actual Number of Executions: 1   (if > 1 → potential issue)

Summary

ParameterYour ValueMeaning
Operator Cost0.001982 (2%)✅ No concern, only 2% of query
I/O Cost0✅ No disk reads, CPU only
Estimated Rows19,820⚠️ Need to compare with Actual Rows
Executions1✅ No loop
Row Size56 B✅ Small, low memory footprint

Conclusion: This Compute Scalar operator is not a problem to optimize. If the query is slow, look for operators with higher cost (table scan, index scan, hash join) or check if Estimated Rows differs significantly from Actual Rows.

Clustered Index Scan

Clustered Index Scan scans the entire clustered index (meaning the entire table, since clustered index stores all data). This is the most expensive operator in the execution plan (taking 97% of the cost).

What is Clustered Index Scan?

When SQL Server performs a Clustered Index Scan, it reads through every row in the clustered index. Since the clustered index contains the entire table data (the data rows are stored in the leaf nodes of the index), a scan means reading the entire table.

This operator is typically the most expensive because it reads all rows, not just a subset.

Common Use Cases

  • No suitable index for the WHERE clause
  • No WHERE clause at all (SELECT * FROM table)
  • Non-sargable WHERE clause (e.g., WHERE YEAR(date) = 2024)
  • Fetching too many rows (SQL Server estimates scan is faster than seek + bookmark lookup)

Parameters Explained

1. Storage

Storage: RowStore
  • Data is stored in row format (horizontal) - traditional storage
  • RowStore vs ColumnStore: RowStore is good for OLTP (many inserts/updates), ColumnStore is good for OLAP (aggregates on many rows)

2. Number of Rows - VERY IMPORTANT

Number of Rows Read: 19820
Actual Number of Rows: 19820
Estimated Number of Rows: 19820
Estimated Number of Rows to be Read: 19820

✅ GOOD POINT:

  • Actual = Estimated = 19,820 rows
  • Statistics are accurate, no cardinality estimation issues

Meaning:

  • SQL Server correctly estimates the number of rows to process
  • Query optimizer selects a plan appropriate for actual data

3. Cost - THE MAIN ISSUE

Estimated Operator Cost: 0.113973 (97%)
Estimated I/O Cost: 0.0920139
Estimated CPU Cost: 0.021959

⚠️ KEY POINTS:

MetricValuePercentageAssessment
Operator Cost0.11397397%❌ Very high
I/O Cost0.092013981% of operator❌ Reading from disk
CPU Cost0.02195919% of operator⚠️ Significant

Analysis:

  • This operator takes 97% of the total query cost
  • 81% of cost is I/O → reading data from disk, not from cache
  • THIS IS THE MAIN BOTTLENECK of the query

4. Execution Parameters

Number of Executions: 1
Actual Rebinds: 0
Actual Rewinds: 0
Ordered: False
ParameterValueMeaning
Executions1Runs only once (not in a loop) ✅
Rebinds0Not re-initializing parameters ❌
Rewinds0No loop join rewind ✅
OrderedFalseResult is not sorted by clustered key

5. Row Size

Estimated Row Size: 47 B
  • Each row ~47 bytes
  • Total data: 47 × 19,820 ≈ 931 KB (less than 1 MB)

⚠️ Paradox:

  • Data is only ~1 MB, but still scanning everything?
  • The table may have more than 19,820 rows but the filter only selects 19,820 rows without a suitable index

Comparison with Compute Scalar

OperatorCostI/O CostCPU CostRows
Clustered Index Scan0.113973 (97%)0.0920140.02195919,820
Compute Scalar0.001982 (2%)00.00198219,820

Observation:

  • Clustered Index Scan is 57 times more expensive than Compute Scalar
  • If the query is slow, the issue is here, not in Compute Scalar

How to Optimize

Method 1: Check if there’s a WHERE clause

-- Current query (assuming)
SELECT * FROM Orders
WHERE CustomerId = 12345  -- If there's no index on CustomerId

Solution: Create a non-clustered index

CREATE INDEX IX_Orders_CustomerId ON Orders(CustomerId)

After that, the plan will become:

Index Seek (NonClustered) → Key Lookup (Clustered) → Compute Scalar

Method 2: If query has no WHERE clause (SELECT all)

SELECT * FROM Orders  -- Fetching all 19,820 rows

Assessment:

  • Scanning 20,000 rows is acceptable (I/O cost 0.09 is small)
  • No optimization needed if data is small

Method 3: Check WHERE clause sargability

-- BAD (non-sargable)
WHERE YEAR(OrderDate) = 2024

-- GOOD (sargable)
WHERE OrderDate >= '2024-01-01' AND OrderDate < '2025-01-01'

Method 4: Use covering index to avoid Key Lookup

If the query only needs a few columns:

-- Instead of SELECT *
CREATE INDEX IX_Orders_CustomerId_Include 
ON Orders(CustomerId) 
INCLUDE (OrderDate, TotalAmount)

The plan will become:

Index Seek (NonClustered) → Compute Scalar
(No Key Lookup needed)

Overall Assessment

✅ Good Points:

MetricValueAssessment
Actual vs Estimated Rows19,820 = 19,820Statistics accurate
Executions1No loop
Row Size47 BSmall data

❌ Points to Improve:

MetricValueIssue
Operator Cost97%Takes almost entire cost
I/O Cost0.092Reading disk, not cache
StorageRowStoreAppropriate but scanning all

Questions to Determine If Optimization is Needed

  1. Does the query have a WHERE clause?

    • If yes: Need to create an index for the column in WHERE
    • If no: Scanning 20k rows is acceptable
  2. What is the total number of rows in the table?

    • If ≈ 20,000: Scan is OK
    • If >> 20,000 (e.g., 1 million): Scan is a serious issue
  3. How long does the query take?

    • If < 100ms: No optimization needed
    • If > 1s: Need to create an index

Summary

This Clustered Index Scan is the main bottleneck (97% cost).

Next steps:

  1. View query text (F4 or hover over operator) to see WHERE clause
  2. Check total rows in the table
  3. If there’s a valid WHERE clause → create non-clustered index
  4. If query actually runs slow (> 500ms) → optimize now
  5. If query is fast (< 100ms) and data is small → can leave as is

Principle: Clustered Index Scan on 20,000 rows is not always bad. What’s important is execution frequency and actual time.

SQL Server Programming (T-SQL Programming)

Phần này tổng hợp kiến thức về lập trình T-SQL trong SQL Server, từ các khái niệm cơ bản đến kỹ thuật nâng cao. Đây là nền tảng quan trọng để xây dựng các ứng dụng database hiệu năng cao và bảo trì dễ dàng.

Các chủ đề chính

Chủ đềMô tả
Stored ProceduresThủ tục lưu trữ, tham số, dynamic SQL, error handling, transactions
User-Defined FunctionsScalar, Inline TVF, Multi-Statement TVF, performance considerations
TriggersDML/DDL/Logon triggers, inserted/deleted tables, anti-patterns
Views, CTEs & Window FunctionsViews, indexed views, CTEs, recursive queries, window functions, cursors

Tổng quan T-SQL Programming

Database Objects trong SQL Server

SQL Server Database Objects
├── Programmability
│   ├── Stored Procedures
│   ├── Functions
│   │   ├── Scalar Functions
│   │   ├── Table-Valued Functions
│   │   └── Aggregate Functions (CLR)
│   ├── Triggers
│   │   ├── DML Triggers
│   │   ├── DDL Triggers
│   │   └── Logon Triggers
│   └── Types
├── Views
│   ├── Standard Views
│   └── Indexed Views (Materialized)
└── Queries
    ├── CTEs (Common Table Expressions)
    ├── Window Functions
    └── Cursors

Khi nào dùng gì?

Tình huốngGiải pháp tốt nhất
Tái sử dụng business logic phức tạpStored Procedure
Tính toán trong SELECT, tái sử dụngInline TVF hoặc Scalar UDF (SQL 2019+)
Tự động audit/validate khi DMLTrigger (thận trọng)
Đơn giản hóa truy vấn phức tạpView hoặc CTE
Aggregation theo partitionWindow Functions
Traversal hierarchyRecursive CTE
Pre-aggregate dữ liệu lớnIndexed View

Q&A - Phỏng vấn T-SQL Programming

🟢 Junior Level

Q1: Stored Procedure là gì? Lợi ích so với việc gửi raw SQL từ application?

A: Stored Procedure (SP) là một tập hợp các câu lệnh T-SQL được biên dịch và lưu trong database. Lợi ích:

  • Code reuse: Nhiều application/user dùng chung logic
  • Security: Cấp quyền EXECUTE thay vì SELECT/INSERT/UPDATE trực tiếp
  • Performance: Execution plan được cache lần đầu, các lần sau tái sử dụng
  • Reduced network traffic: Chỉ gửi tên SP và tham số thay vì toàn bộ SQL text
  • Maintainability: Thay đổi logic ở một nơi
-- Thay vì gửi raw SQL từ app:
-- SELECT * FROM Orders WHERE CustomerId = 1 AND Status = 'Active'

-- Dùng SP:
EXEC sp_GetActiveOrders @CustomerId = 1;

Q2: Sự khác nhau giữa Scalar Function và Table-Valued Function?

A:

Tiêu chíScalar FunctionTable-Valued Function
Trả vềMột giá trị đơnMột tập kết quả (table)
Dùng trongSELECT, WHERE, JOINFROM clause
PerformanceChậm (row-by-row)Nhanh hơn (set-based với iTVF)
Ví dụdbo.GetAge(BirthDate)dbo.GetOrdersByCustomer(1)

Q3: Trigger là gì? Có mấy loại trigger trong SQL Server?

A: Trigger là một loại stored procedure đặc biệt, tự động thực thi khi xảy ra một sự kiện nhất định.

  • DML Triggers: Kích hoạt bởi INSERT, UPDATE, DELETE
    • AFTER trigger: Chạy sau khi DML hoàn thành
    • INSTEAD OF trigger: Thay thế DML gốc
  • DDL Triggers: Kích hoạt bởi CREATE, ALTER, DROP
  • Logon Triggers: Kích hoạt khi user đăng nhập

Q4: View là gì? Khi nào nên dùng View?

A: View là một virtual table được định nghĩa bởi một câu SELECT. View không lưu dữ liệu (trừ Indexed View).

Nên dùng khi:

  • Đơn giản hóa câu truy vấn phức tạp cho user
  • Ẩn các cột nhạy cảm (security layer)
  • Cung cấp interface ổn định khi schema thay đổi
  • Tổng hợp dữ liệu từ nhiều bảng thường xuyên truy vấn

Q5: CTE là gì? Cú pháp cơ bản?

A: CTE (Common Table Expression) là một named temporary result set được định nghĩa trong phạm vi của một câu query.

WITH CTE_Name AS (
    SELECT column1, column2
    FROM TableName
    WHERE condition
)
SELECT * FROM CTE_Name;

CTE giúp code dễ đọc hơn, thay thế subquery phức tạp.


Q6: Window Function là gì? Cho ví dụ ROW_NUMBER()?

A: Window Function thực hiện tính toán trên một tập các rows liên quan đến row hiện tại (một “window”), không làm giảm số rows kết quả như GROUP BY.

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

Q7: Sự khác nhau giữa RANK(), DENSE_RANK() và ROW_NUMBER()?

A:

-- Dữ liệu: Salary = 5000, 4000, 4000, 3000
SELECT
    Salary,
    ROW_NUMBER() OVER (ORDER BY Salary DESC) AS RowNum,   -- 1, 2, 3, 4
    RANK()       OVER (ORDER BY Salary DESC) AS Rnk,      -- 1, 2, 2, 4
    DENSE_RANK() OVER (ORDER BY Salary DESC) AS DenseRnk  -- 1, 2, 2, 3
FROM Salaries;
  • ROW_NUMBER: Luôn unique, không có ties
  • RANK: Ties có cùng rank, skip numbers sau đó
  • DENSE_RANK: Ties có cùng rank, không skip numbers

Q8: Cursor là gì? Tại sao nên tránh dùng cursor?

A: Cursor cho phép xử lý từng row một (row-by-row) thay vì set-based. Nên tránh vì:

  • Chậm hơn set-based operations nhiều lần
  • Tốn tài nguyên (memory, temp storage)
  • Gây lock lâu hơn
  • Hầu hết trường hợp đều có thể thay bằng set-based SQL hoặc window functions

Q9: DECLARE và SET khác nhau như thế nào với SELECT khi gán biến?

A:

DECLARE @Name NVARCHAR(100);

-- SET: chỉ gán một giá trị, nếu query trả về nhiều row thì lỗi
SET @Name = (SELECT Name FROM Employees WHERE Id = 1);

-- SELECT: gán giá trị cuối cùng nếu nhiều rows
SELECT @Name = Name FROM Employees WHERE DepartmentId = 5;
-- Nếu không có row nào thỏa điều kiện:
-- SET -> @Name = NULL
-- SELECT -> @Name giữ nguyên giá trị cũ (nguy hiểm!)

Q10: @@ROWCOUNT và @@ERROR dùng để làm gì?

A:

  • @@ROWCOUNT: Số dòng bị ảnh hưởng bởi câu lệnh cuối cùng
  • @@ERROR: Error number của lỗi cuối (0 nếu không có lỗi)
UPDATE Orders SET Status = 'Shipped' WHERE OrderId = 100;
IF @@ROWCOUNT = 0
    PRINT 'Không tìm thấy Order';

Lưu ý: @@ERROR bị reset sau mỗi câu lệnh. Dùng TRY...CATCH thay thế trong code hiện đại.


🟡 Mid Level

Q11: Parameter Sniffing trong Stored Procedure là gì? Cách xử lý?

A: Parameter Sniffing là cơ chế SQL Server compile execution plan dựa trên giá trị tham số đầu tiên được sử dụng. Plan tốt cho giá trị đó có thể rất tệ cho giá trị khác.

-- Vấn đề: Compile với @CustomerId = 1 (ít orders)
-- Plan dùng Index Seek. Nhưng khi gọi với @CustomerId = 999 (nhiều orders)
-- vẫn dùng plan cũ -> chậm

CREATE PROCEDURE GetCustomerOrders @CustomerId INT
AS
    SELECT * FROM Orders WHERE CustomerId = @CustomerId;

-- Giải pháp 1: OPTION (RECOMPILE) - recompile mỗi lần
CREATE PROCEDURE GetCustomerOrders @CustomerId INT
AS
    SELECT * FROM Orders WHERE CustomerId = @CustomerId
    OPTION (RECOMPILE);

-- Giải pháp 2: Local variables (ngăn sniffing, mất một số optimization)
CREATE PROCEDURE GetCustomerOrders @CustomerId INT
AS
    DECLARE @LocalCustomerId INT = @CustomerId;
    SELECT * FROM Orders WHERE CustomerId = @LocalCustomerId;

-- Giải pháp 3: WITH RECOMPILE ở SP level
CREATE PROCEDURE GetCustomerOrders @CustomerId INT
WITH RECOMPILE
AS
    SELECT * FROM Orders WHERE CustomerId = @CustomerId;

Q12: Dynamic SQL là gì? sp_executesql có lợi ích gì so với EXEC()?

A: Dynamic SQL là SQL được xây dựng và thực thi lúc runtime.

-- EXEC() - không parameterized, dễ SQL injection
EXEC ('SELECT * FROM ' + @TableName + ' WHERE Id = ' + @Id);

-- sp_executesql - parameterized, safe, plan reuse
DECLARE @SQL NVARCHAR(MAX);
DECLARE @Params NVARCHAR(500);

SET @SQL = N'SELECT * FROM Orders WHERE CustomerId = @CustId AND Status = @Status';
SET @Params = N'@CustId INT, @Status NVARCHAR(50)';

EXEC sp_executesql @SQL, @Params, 
    @CustId = @CustomerId, 
    @Status = @OrderStatus;

Lợi ích của sp_executesql:

  1. Parameterized: Ngăn SQL injection
  2. Plan caching: Cùng SQL text → tái sử dụng execution plan
  3. Output parameters: Có thể nhận giá trị trả về

Q13: Indexed View (Materialized View) là gì? Yêu cầu để tạo Indexed View?

A: Indexed View là view có dữ liệu được vật lý hóa (stored) trên disk, cập nhật tự động khi base tables thay đổi.

Yêu cầu bắt buộc:

  1. WITH SCHEMABINDING trong CREATE VIEW
  2. Unique Clustered Index là index đầu tiên tạo trên view
  3. Các function phải deterministic
  4. Không dùng *, DISTINCT, TOP, OUTER JOIN, subqueries, CTEs
  5. SET ANSI_NULLS ONSET QUOTED_IDENTIFIER ON
CREATE VIEW dbo.vw_SalesSummary
WITH SCHEMABINDING
AS
    SELECT 
        p.CategoryId,
        COUNT_BIG(*) AS TotalOrders,
        SUM(od.Quantity * od.UnitPrice) AS TotalRevenue
    FROM dbo.OrderDetails od
    INNER JOIN dbo.Products p ON od.ProductId = p.ProductId
    GROUP BY p.CategoryId;
GO

-- Tạo unique clustered index để materialize
CREATE UNIQUE CLUSTERED INDEX IX_vw_SalesSummary
ON dbo.vw_SalesSummary (CategoryId);

Q14: Recursive CTE hoạt động như thế nào? Viết query duyệt cây phân cấp?

A: Recursive CTE gồm hai phần:

  • Anchor member: Query không đệ quy, trả về điểm bắt đầu
  • Recursive member: Query tham chiếu chính CTE đó
-- Bảng phân cấp nhân viên
-- EmployeeId, Name, ManagerId

WITH EmployeeHierarchy AS (
    -- Anchor: CEO (không có manager)
    SELECT 
        EmployeeId, Name, ManagerId,
        0 AS Level,
        CAST(Name AS NVARCHAR(MAX)) AS Path
    FROM Employees
    WHERE ManagerId IS NULL

    UNION ALL

    -- Recursive: tìm direct reports
    SELECT 
        e.EmployeeId, e.Name, e.ManagerId,
        eh.Level + 1,
        eh.Path + ' > ' + e.Name
    FROM Employees e
    INNER JOIN EmployeeHierarchy eh ON e.ManagerId = eh.EmployeeId
)
SELECT EmployeeId, Name, Level, Path
FROM EmployeeHierarchy
OPTION (MAXRECURSION 100); -- giới hạn độ sâu đệ quy

Q15: Sự khác nhau giữa AFTER trigger và INSTEAD OF trigger?

A:

AFTER TriggerINSTEAD OF Trigger
Thời điểmSau khi DML hoàn thànhThay thế DML, không chạy DML gốc
Có thể rollback?Có (nằm trong cùng transaction)Phải tự thực hiện DML nếu muốn
Áp dụng trênTablesTables và Views
Dùng choAudit, cascade business rulesUpdatable views, complex validation
-- INSTEAD OF trigger trên view cho phép update
CREATE TRIGGER trg_InsertOrderSummary
ON vw_OrderSummary
INSTEAD OF INSERT
AS
BEGIN
    -- Tự viết logic INSERT vào base tables
    INSERT INTO Orders (CustomerId, OrderDate)
    SELECT CustomerId, OrderDate FROM inserted;
END;

Q16: LAG() và LEAD() dùng để làm gì? Cho ví dụ tính month-over-month growth?

A:

-- LAG: lấy giá trị từ row trước
-- LEAD: lấy giá trị từ row sau
SELECT
    Month,
    Revenue,
    LAG(Revenue, 1, 0) OVER (ORDER BY Month) AS PrevMonthRevenue,
    LEAD(Revenue, 1, 0) OVER (ORDER BY Month) AS NextMonthRevenue,
    Revenue - LAG(Revenue, 1, 0) OVER (ORDER BY Month) AS MoMChange,
    CASE 
        WHEN LAG(Revenue, 1, 0) OVER (ORDER BY Month) = 0 THEN NULL
        ELSE CAST(
            (Revenue - LAG(Revenue, 1, 0) OVER (ORDER BY Month)) * 100.0 
            / LAG(Revenue, 1, 0) OVER (ORDER BY Month) 
            AS DECIMAL(10,2))
    END AS MoMGrowthPct
FROM MonthlySales
ORDER BY Month;

Q17: Running Total (Cumulative Sum) tính như thế nào với Window Functions?

A:

SELECT
    OrderDate,
    OrderAmount,
    SUM(OrderAmount) OVER (
        ORDER BY OrderDate
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS RunningTotal
FROM Orders;

-- So sánh ROWS vs RANGE:
-- ROWS BETWEEN: tính theo vị trí vật lý (exact)
-- RANGE BETWEEN: tính theo giá trị (bao gồm ties)
-- ROWS thường hiệu năng tốt hơn

Q18: Khi nào dùng CTE, khi nào dùng Temp Table, khi nào dùng Table Variable?

A:

CTETemp TableTable Variable
ScopeTrong querySessionBatch/SP
StatisticsKhông cóCó (auto)Không có (trước 2019)
IndexKhôngCó thể tạoChỉ PK/UK
Reuse nhiều lầnKhông (re-evaluate)
Transaction logKhông riêng biệtTempDBTempDB
Lớn hơn 1000 rowsNên dùng Temp Table
Recursive

Q19: SCHEMABINDING trong View và Function có tác dụng gì?

A: WITH SCHEMABINDING ràng buộc object (view/function) với schema của các base objects.

  • Không thể DROP hoặc ALTER base tables/columns khi chưa DROP view/function trước
  • Bắt buộc để tạo Indexed View
  • Cho phép function/view được đánh dấu là deterministic → có thể dùng trong computed column, index
-- Không thể DROP Products.Price nếu còn view này
CREATE VIEW dbo.vw_ProductPrices
WITH SCHEMABINDING
AS
    SELECT ProductId, ProductName, Price
    FROM dbo.Products; -- Phải dùng schema name (dbo.)

Q20: TRY…CATCH trong T-SQL hoạt động như thế nào?

A:

BEGIN TRY
    BEGIN TRANSACTION;
    
    INSERT INTO Orders (CustomerId, Amount) VALUES (1, 500);
    UPDATE Inventory SET Stock = Stock - 1 WHERE ProductId = 10;
    
    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    IF @@TRANCOUNT > 0
        ROLLBACK TRANSACTION;
    
    -- Lấy thông tin lỗi
    SELECT
        ERROR_NUMBER()    AS ErrorNumber,
        ERROR_MESSAGE()   AS ErrorMessage,
        ERROR_SEVERITY()  AS Severity,
        ERROR_STATE()     AS State,
        ERROR_LINE()      AS ErrorLine,
        ERROR_PROCEDURE() AS ErrorProcedure;
    
    -- Re-throw lỗi (SQL 2012+)
    THROW;
END CATCH;

Q21: RAISERROR và THROW khác nhau như thế nào?

A:

RAISERRORTHROW
Cú phápRAISERROR(msg, severity, state)THROW [error_number, message, state]
SeverityCần chỉ địnhMặc định 16
Re-throwRAISERROR với @ErrorMessageTHROW; (không tham số)
Error number tùy chỉnhCần trong sys.messagesBất kỳ số >= 50000
SQL versionSQL 2012+ (khuyến dùng)
-- THROW re-throw trong CATCH
BEGIN CATCH
    ROLLBACK TRANSACTION;
    THROW; -- Re-throws original error với full context
END CATCH;

-- RAISERROR custom message
RAISERROR('Số lượng không được âm', 16, 1);

-- THROW custom
THROW 50001, 'Số lượng không được âm', 1;

🔴 Senior Level

Q22: Giải thích vấn đề “Halloween Protection” trong SQL Server và ảnh hưởng đến trigger?

A: Halloween Protection là cơ chế SQL Server đảm bảo rằng một row không bị thay đổi nhiều lần trong cùng một câu DML. SQL Server đôi khi phải dùng eager spool operator, có thể ảnh hưởng hiệu năng.

Với trigger, inserteddeleted tables chứa snapshot của rows bị ảnh hưởng, không phải toàn bộ bảng. Trigger phải xử lý multi-row operations:

-- Anti-pattern: Giả sử chỉ một row bị UPDATE
CREATE TRIGGER trg_AfterUpdate ON Orders AFTER UPDATE
AS
BEGIN
    DECLARE @OrderId INT = (SELECT OrderId FROM inserted); -- SAI khi nhiều rows!
    UPDATE OrderAudit SET ModifiedDate = GETDATE() WHERE OrderId = @OrderId;
END;

-- Đúng: Xử lý set-based
CREATE TRIGGER trg_AfterUpdate ON Orders AFTER UPDATE
AS
BEGIN
    UPDATE oa
    SET ModifiedDate = GETDATE()
    FROM OrderAudit oa
    INNER JOIN inserted i ON oa.OrderId = i.OrderId;
END;

Q23: Scalar UDF Inlining trong SQL Server 2019 là gì? Điều kiện để một UDF được inline?

A: Scalar UDF Inlining là tính năng SQL 2019 tự động chuyển đổi eligible scalar UDFs thành relational expressions (như inline views), cho phép:

  • Parallelism (trước đây UDF bắt query dùng single-thread)
  • Cost-based optimization
  • Push predicates vào UDF

Điều kiện để được inline:

  1. Chỉ có một RETURN statement
  2. Không dùng EXECUTE
  3. Không dùng recursive calls
  4. Không dùng TRY...CATCH
  5. Không dùng table variables
  6. Không có side effects (chỉ SELECT)
  7. Không gọi các functions không deterministic như RAND(), NEWID()
-- Kiểm tra UDF có được inline không
SELECT 
    name,
    is_inlineable
FROM sys.sql_modules sm
JOIN sys.objects o ON sm.object_id = o.object_id
WHERE o.type = 'FN';

-- Force inline (nếu không eligible tự động)
SELECT dbo.GetDiscount(ProductId) WITH INLINE = ON
-- hoặc disable:
SELECT dbo.GetDiscount(ProductId) WITH INLINE = OFF

Q24: Giải thích ROWS BETWEEN vs RANGE BETWEEN trong Window Functions. Performance implications?

A:

-- ROWS: frame theo vị trí vật lý (exact row count)
SUM(Amount) OVER (ORDER BY Date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)

-- RANGE: frame theo giá trị (bao gồm tất cả rows cùng giá trị ORDER BY)
SUM(Amount) OVER (ORDER BY Date RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)

Sự khác biệt với ties:

  • Nếu có nhiều rows cùng Date:
    • ROWS: Running total tính đến chính xác row hiện tại
    • RANGE: Running total bao gồm tất cả rows cùng Date

Performance:

  • RANGE BETWEEN yêu cầu spool operator (lưu tạm kết quả) → chậm hơn
  • ROWS BETWEEN hiệu quả hơn, ít tốn memory hơn
  • Khi không chỉ định: default là RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW → nên luôn explicit dùng ROWS

Q25: CDC (Change Data Capture) vs Triggers: khi nào dùng cái nào?

A:

TriggersCDC
ImplementationT-SQL manualSQL Server built-in
OverheadSynchronous, trong transactionAsynchronous, đọc transaction log
Impact on DMLCó (tăng latency)Minimal (async)
GranularityRow + column level tùy chọnRow + column level
HistoryPhải tự buildBuilt-in change table
SetupĐơn giảnPhức tạp hơn
Use caseReal-time validation/cascadeAudit, ETL, data sync

CDC phù hợp hơn khi:

  • Cần audit history mà không muốn ảnh hưởng performance
  • ETL pipeline cần biết changed data
  • Replication-like scenarios

Trigger phù hợp hơn khi:

  • Cần real-time validation/enforcement
  • Cần cascade business logic ngay lập tức
  • Cần reject DML dựa trên điều kiện phức tạp

Q26: SAVE TRANSACTION (Savepoint) hoạt động như thế nào? Khi nào dùng?

A:

BEGIN TRANSACTION OuterTran;

    INSERT INTO AuditLog (Action) VALUES ('Start process');

    SAVE TRANSACTION SavePoint1; -- Tạo savepoint

    BEGIN TRY
        INSERT INTO Orders (CustomerId) VALUES (999); -- Có thể lỗi
        
        SAVE TRANSACTION SavePoint2;
        
        UPDATE Inventory SET Stock = Stock - 1 WHERE ProductId = 1;
        
    END TRY
    BEGIN CATCH
        -- Rollback về savepoint, không rollback toàn bộ
        ROLLBACK TRANSACTION SavePoint1;
        -- AuditLog INSERT vẫn còn
        INSERT INTO AuditLog (Action) VALUES ('Order failed: ' + ERROR_MESSAGE());
    END CATCH;

COMMIT TRANSACTION OuterTran;

Lưu ý quan trọng:

  • ROLLBACK TRANSACTION SavepointName chỉ undo đến savepoint, không kết thúc transaction
  • @@TRANCOUNT không thay đổi khi rollback về savepoint
  • Savepoint không thực sự “commit” data, data chỉ commit khi outer transaction commit

Q27: COLUMNS_UPDATED() và UPDATE() function trong trigger dùng như thế nào?

A:

CREATE TRIGGER trg_OrderUpdate ON Orders AFTER UPDATE
AS
BEGIN
    -- UPDATE(column): kiểm tra column có trong UPDATE statement không
    IF UPDATE(Status) OR UPDATE(Amount)
    BEGIN
        INSERT INTO OrderAuditLog (OrderId, ChangedBy, ChangeTime)
        SELECT i.OrderId, SYSTEM_USER, GETDATE()
        FROM inserted i;
    END;

    -- COLUMNS_UPDATED(): bitmap của columns được update
    -- Ít dùng hơn vì phức tạp, phụ thuộc vào ordinal position
    IF (COLUMNS_UPDATED() & 4) = 4 -- Column thứ 3 (bit 3 = 4)
    BEGIN
        -- Column 3 được update
    END;
END;

Q28: Nested Stored Procedures và @@NESTLEVEL. Giới hạn là bao nhiêu?

A:

  • SQL Server cho phép tối đa 32 levels của nested procedure calls
  • @@NESTLEVEL trả về level hiện tại (0 = top level, 1 = called from top-level SP, …)
CREATE PROCEDURE dbo.OuterProc
AS
BEGIN
    SELECT @@NESTLEVEL; -- Returns 1
    EXEC dbo.InnerProc;
END;

CREATE PROCEDURE dbo.InnerProc
AS
BEGIN
    SELECT @@NESTLEVEL; -- Returns 2
    
    IF @@NESTLEVEL > 16
    BEGIN
        THROW 50001, 'Quá nhiều nested calls', 1;
    END;
END;

Q29: Gaps and Islands problem - giải quyết bằng Window Functions?

A: Bài toán tìm các dải liên tiếp (islands) và khoảng trống (gaps) trong dữ liệu.

-- Dữ liệu: LoginDate của user
-- Tìm các "dải" ngày liên tiếp user login

WITH LoginData AS (
    SELECT 
        UserId,
        LoginDate,
        ROW_NUMBER() OVER (PARTITION BY UserId ORDER BY LoginDate) AS RowNum
    FROM UserLogins
),
Islands AS (
    SELECT
        UserId,
        LoginDate,
        DATEADD(DAY, -RowNum, LoginDate) AS IslandGroup -- Ngày trừ đi row number
    FROM LoginData
)
SELECT
    UserId,
    MIN(LoginDate) AS IslandStart,
    MAX(LoginDate) AS IslandEnd,
    DATEDIFF(DAY, MIN(LoginDate), MAX(LoginDate)) + 1 AS ConsecutiveDays
FROM Islands
GROUP BY UserId, IslandGroup
ORDER BY UserId, IslandStart;

Q30: Giải thích “deferred name resolution” trong Stored Procedures. Ảnh hưởng gì?

A: SQL Server không verify tên của objects trong stored procedure tại thời điểm CREATE PROCEDURE, mà chỉ resolve khi SP được thực thi. Điều này có nghĩa:

-- Tạo SP thành công dù NonExistentTable chưa tồn tại
CREATE PROCEDURE dbo.MyProc
AS
    SELECT * FROM NonExistentTable; -- Không lỗi khi CREATE
GO

-- Chỉ lỗi khi EXEC
EXEC dbo.MyProc; -- Lỗi: Invalid object name 'NonExistentTable'

Ảnh hưởng:

  • Cho phép tạo SP trước khi tables tồn tại (useful trong deployment scenarios)
  • Có thể tạo ra lỗi runtime khó debug
  • Ngoại lệ: Column names và data types được resolve lúc compile → lỗi nếu column không tồn tại

Best practice: Dùng sys.sql_modules hoặc sp_refreshsqlmodule để refresh dependencies sau khi schema thay đổi.


Q31: Viết query phân trang (pagination) hiệu năng cao với Window Functions?

A:

-- Cách 1: OFFSET/FETCH (SQL 2012+) - đơn giản nhưng chậm với large offset
SELECT *
FROM Products
ORDER BY ProductId
OFFSET (@Page - 1) * @PageSize ROWS
FETCH NEXT @PageSize ROWS ONLY;

-- Cách 2: Keyset pagination (Seek method) - hiệu năng cao
-- Dùng giá trị cuối của page trước thay vì OFFSET
SELECT TOP (@PageSize) *
FROM Products
WHERE ProductId > @LastProductId -- @LastProductId = ProductId cuối page trước
ORDER BY ProductId;

-- Cách 3: ROW_NUMBER() approach
WITH PagedData AS (
    SELECT
        *,
        ROW_NUMBER() OVER (ORDER BY ProductId) AS RowNum
    FROM Products
)
SELECT *
FROM PagedData
WHERE RowNum BETWEEN (@Page - 1) * @PageSize + 1 AND @Page * @PageSize;

So sánh hiệu năng:

  • OFFSET/FETCH: Tốt cho small-medium datasets, dễ implement
  • Keyset: Tốt nhất cho large datasets, nhưng không hỗ trợ random page access
  • ROW_NUMBER(): Linh hoạt nhưng phải tính toán toàn bộ resultset

Q32: Sự khác nhau giữa FAST_FORWARD cursor và SET-BASED operations. Khi nào cursor thực sự cần thiết?

A: FAST_FORWARD là cursor type nhanh nhất (read-only, forward-only), nhưng vẫn chậm hơn set-based nhiều.

Cursor thực sự cần khi:

  1. Gọi Stored Procedure cho mỗi row (không thể set-based)
  2. Cần gửi kết quả row-by-row qua network stream
  3. Logic quá phức tạp không thể biểu diễn bằng set-based
-- Cursor FAST_FORWARD (nhanh nhất nếu phải dùng cursor)
DECLARE @OrderId INT;

DECLARE cur CURSOR FAST_FORWARD FOR
    SELECT OrderId FROM Orders WHERE Status = 'Pending';

OPEN cur;
FETCH NEXT FROM cur INTO @OrderId;

WHILE @@FETCH_STATUS = 0
BEGIN
    EXEC ProcessOrder @OrderId; -- Gọi SP cho từng row
    FETCH NEXT FROM cur INTO @OrderId;
END;

CLOSE cur;
DEALLOCATE cur;

-- Thay thế bằng: EXECUTE dùng STRING_AGG hoặc XML để batch

Q33: Giải thích cách SQL Server cache execution plans. Khi nào plan bị invalidated?

A:

Plan caching mechanism:

  1. SQL text được hash → tìm trong plan cache
  2. Nếu tìm thấy → reuse (soft parse)
  3. Nếu không → compile → lưu vào cache (hard parse)

Plan bị invalidated/evicted khi:

  • Statistics được update (UPDATE STATISTICS)
  • Index được tạo/drop/rebuild
  • sp_recompile được gọi
  • DBCC FREEPROCCACHE được chạy
  • Bộ nhớ áp lực cao (cache eviction)
  • Schema của referenced objects thay đổi
  • SET options thay đổi (ANSI_NULLS, etc.)
-- Xem plan cache
SELECT 
    qs.execution_count,
    qs.total_elapsed_time / qs.execution_count AS avg_elapsed_time,
    SUBSTRING(qt.text, qs.statement_start_offset/2, 
        (CASE WHEN qs.statement_end_offset = -1 
              THEN LEN(CONVERT(NVARCHAR(MAX), qt.text)) * 2 
              ELSE qs.statement_end_offset END - qs.statement_start_offset)/2) AS query_text,
    qp.query_plan
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) qp
ORDER BY avg_elapsed_time DESC;

Stored Procedures

Stored Procedure (SP) là một tập hợp các câu lệnh T-SQL được biên dịch, đặt tên và lưu trữ trong database. SP là công cụ lập trình quan trọng nhất trong SQL Server, cho phép đóng gói business logic, tăng bảo mật và cải thiện hiệu năng.

1. Lợi ích của Stored Procedures

Lợi íchGiải thích
Code ReuseViết một lần, gọi từ nhiều nơi
SecurityCấp quyền EXECUTE thay vì trực tiếp trên tables
PerformanceExecution plan được compile và cache
Reduced Network TrafficChỉ truyền tên SP + tham số
MaintainabilityThay đổi logic ở một chỗ
AbstractionẨn schema chi tiết khỏi application

Execution Plan Caching

-- Lần đầu: SQL Server compile SP → lưu plan vào cache
EXEC dbo.GetCustomerOrders @CustomerId = 1;
-- Những lần sau: reuse plan từ cache (nhanh hơn)
EXEC dbo.GetCustomerOrders @CustomerId = 2;

-- Xem execution plan trong cache
SELECT 
    cp.usecounts,
    cp.objtype,
    qt.text
FROM sys.dm_exec_cached_plans cp
CROSS APPLY sys.dm_exec_sql_text(cp.plan_handle) qt
WHERE qt.text LIKE '%GetCustomerOrders%';

2. Tạo và Gọi Stored Procedure

CREATE PROCEDURE

-- Cú pháp cơ bản
CREATE PROCEDURE dbo.GetActiveOrders
AS
BEGIN
    SET NOCOUNT ON; -- Tắt "X rows affected", giảm network traffic

    SELECT 
        OrderId,
        CustomerId,
        OrderDate,
        TotalAmount
    FROM Orders
    WHERE Status = 'Active'
    ORDER BY OrderDate DESC;
END;
GO

ALTER PROCEDURE và DROP PROCEDURE

-- Thay đổi SP (giữ nguyên permissions)
ALTER PROCEDURE dbo.GetActiveOrders
AS
BEGIN
    SET NOCOUNT ON;
    SELECT * FROM Orders WHERE Status = 'Active' AND IsDeleted = 0;
END;
GO

-- Xóa SP
DROP PROCEDURE IF EXISTS dbo.GetActiveOrders; -- SQL 2016+
-- hoặc
IF OBJECT_ID('dbo.GetActiveOrders', 'P') IS NOT NULL
    DROP PROCEDURE dbo.GetActiveOrders;

Input Parameters

CREATE PROCEDURE dbo.GetOrdersByCustomer
    @CustomerId     INT,
    @Status         NVARCHAR(50),
    @StartDate      DATE = NULL, -- Tham số có giá trị mặc định
    @EndDate        DATE = NULL,
    @MaxRows        INT = 100
AS
BEGIN
    SET NOCOUNT ON;

    SELECT TOP (@MaxRows)
        o.OrderId,
        o.OrderDate,
        o.TotalAmount,
        c.CustomerName
    FROM Orders o
    INNER JOIN Customers c ON o.CustomerId = c.CustomerId
    WHERE 
        o.CustomerId = @CustomerId
        AND o.Status = @Status
        AND (@StartDate IS NULL OR o.OrderDate >= @StartDate)
        AND (@EndDate IS NULL OR o.OrderDate <= @EndDate)
    ORDER BY o.OrderDate DESC;
END;
GO

-- Gọi SP với named parameters (khuyến cáo)
EXEC dbo.GetOrdersByCustomer
    @CustomerId = 1,
    @Status = 'Shipped',
    @StartDate = '2025-01-01';

-- Gọi với positional parameters (không khuyến cáo)
EXEC dbo.GetOrdersByCustomer 1, 'Shipped';

OUTPUT Parameters

CREATE PROCEDURE dbo.CreateOrder
    @CustomerId     INT,
    @TotalAmount    DECIMAL(18,2),
    @OrderId        INT OUTPUT,      -- OUTPUT parameter
    @ErrorMessage   NVARCHAR(500) OUTPUT
AS
BEGIN
    SET NOCOUNT ON;

    SET @ErrorMessage = NULL;

    IF @TotalAmount <= 0
    BEGIN
        SET @ErrorMessage = 'TotalAmount phải lớn hơn 0';
        SET @OrderId = -1;
        RETURN -1; -- Return code
    END;

    INSERT INTO Orders (CustomerId, TotalAmount, OrderDate, Status)
    VALUES (@CustomerId, @TotalAmount, GETDATE(), 'Pending');

    SET @OrderId = SCOPE_IDENTITY(); -- Lấy ID vừa insert

    RETURN 0; -- Thành công
END;
GO

-- Gọi SP với OUTPUT parameters
DECLARE @NewOrderId INT;
DECLARE @ErrMsg NVARCHAR(500);
DECLARE @ReturnCode INT;

EXEC @ReturnCode = dbo.CreateOrder
    @CustomerId   = 5,
    @TotalAmount  = 299.99,
    @OrderId      = @NewOrderId OUTPUT,
    @ErrorMessage = @ErrMsg OUTPUT;

IF @ReturnCode = 0
    PRINT 'Order created: ' + CAST(@NewOrderId AS VARCHAR);
ELSE
    PRINT 'Error: ' + @ErrMsg;

EXEC vs EXECUTE

-- Hai cú pháp tương đương
EXEC dbo.GetActiveOrders;
EXECUTE dbo.GetActiveOrders;

-- EXEC cũng có thể chạy dynamic string
EXEC ('SELECT GETDATE()');

3. Control Flow

IF/ELSE

CREATE PROCEDURE dbo.ProcessPayment
    @OrderId    INT,
    @Amount     DECIMAL(18,2)
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @CurrentBalance DECIMAL(18,2);

    SELECT @CurrentBalance = Balance 
    FROM CustomerAccounts 
    WHERE CustomerId = (SELECT CustomerId FROM Orders WHERE OrderId = @OrderId);

    IF @CurrentBalance >= @Amount
    BEGIN
        -- Block code khi điều kiện đúng
        UPDATE CustomerAccounts 
        SET Balance = Balance - @Amount
        WHERE CustomerId = (SELECT CustomerId FROM Orders WHERE OrderId = @OrderId);

        UPDATE Orders SET Status = 'Paid' WHERE OrderId = @OrderId;
        PRINT 'Thanh toán thành công';
    END
    ELSE IF @CurrentBalance > 0
    BEGIN
        PRINT 'Số dư không đủ. Còn: ' + CAST(@CurrentBalance AS VARCHAR(20));
    END
    ELSE
    BEGIN
        PRINT 'Tài khoản không có số dư';
    END;
END;

WHILE, BREAK, CONTINUE

CREATE PROCEDURE dbo.GenerateMonthlySummary
    @Year INT
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @Month INT = 1;

    WHILE @Month <= 12
    BEGIN
        -- CONTINUE: bỏ qua tháng nếu không có dữ liệu
        IF NOT EXISTS (SELECT 1 FROM Orders 
                       WHERE YEAR(OrderDate) = @Year 
                       AND MONTH(OrderDate) = @Month)
        BEGIN
            SET @Month = @Month + 1;
            CONTINUE;
        END;

        -- Xử lý tháng hiện tại
        INSERT INTO MonthlySummary (Year, Month, TotalOrders, TotalRevenue)
        SELECT 
            @Year,
            @Month,
            COUNT(*),
            SUM(TotalAmount)
        FROM Orders
        WHERE YEAR(OrderDate) = @Year AND MONTH(OrderDate) = @Month;

        -- BREAK: dừng vòng lặp nếu đủ 6 tháng đầu
        IF @Month = 6
        BEGIN
            BREAK;
        END;

        SET @Month = @Month + 1;
    END;
END;

RETURN

CREATE PROCEDURE dbo.ValidateCustomer
    @CustomerId INT
AS
BEGIN
    IF @CustomerId <= 0
    BEGIN
        RETURN -1; -- Return code âm = lỗi
    END;

    IF NOT EXISTS (SELECT 1 FROM Customers WHERE CustomerId = @CustomerId AND IsActive = 1)
    BEGIN
        RETURN -2; -- Customer không tồn tại hoặc inactive
    END;

    -- Tiếp tục xử lý...
    RETURN 0; -- Thành công
END;
GO

DECLARE @Result INT;
EXEC @Result = dbo.ValidateCustomer @CustomerId = 5;
IF @Result <> 0
    PRINT 'Validation failed with code: ' + CAST(@Result AS VARCHAR);

GOTO (Tại sao nên tránh)

-- GOTO làm code khó đọc, khó maintain - NÊN TRÁNH
CREATE PROCEDURE dbo.ProcessWithGoto
AS
BEGIN
    DECLARE @Counter INT = 0;
    
    StartLoop:
        IF @Counter >= 10 GOTO EndLoop;
        SET @Counter = @Counter + 1;
        
        IF @Counter = 5 GOTO SkipFive;
        PRINT @Counter;
        
    SkipFive:
        GOTO StartLoop;
    
    EndLoop:
        PRINT 'Done';
END;

-- THAY THẾ BẰNG: WHILE với CONTINUE là rõ ràng hơn

4. Variables

CREATE PROCEDURE dbo.CalculateOrderStats
    @CustomerId INT
AS
BEGIN
    SET NOCOUNT ON;

    -- DECLARE nhiều biến
    DECLARE 
        @TotalOrders    INT,
        @TotalSpent     DECIMAL(18,2),
        @AverageOrder   DECIMAL(18,2),
        @CustomerName   NVARCHAR(200);

    -- SET: gán một giá trị cụ thể
    SET @CustomerName = (
        SELECT CustomerName 
        FROM Customers 
        WHERE CustomerId = @CustomerId
    );

    -- SELECT: gán từ query (linh hoạt hơn cho multiple columns)
    SELECT 
        @TotalOrders  = COUNT(*),
        @TotalSpent   = SUM(TotalAmount)
    FROM Orders
    WHERE CustomerId = @CustomerId AND Status = 'Completed';

    -- Tính toán
    SET @AverageOrder = CASE 
        WHEN @TotalOrders > 0 THEN @TotalSpent / @TotalOrders 
        ELSE 0 
    END;

    -- Kết quả
    SELECT 
        @CustomerName AS CustomerName,
        @TotalOrders  AS TotalOrders,
        @TotalSpent   AS TotalSpent,
        @AverageOrder AS AverageOrderValue;
END;

Lưu ý quan trọng: Nếu SELECT @Variable = column FROM table WHERE condition không tìm thấy row nào, biến giữ nguyên giá trị trước đó (không về NULL). Đây là một nguy cơ bug phổ biến!


5. Temp Tables trong Stored Procedures

Local Temp Table (#) vs Global Temp Table (##)

CREATE PROCEDURE dbo.ProcessLargeDataset
    @BatchSize INT = 1000
AS
BEGIN
    SET NOCOUNT ON;

    -- LOCAL temp table: chỉ visible trong session hiện tại
    -- Tự động drop khi SP kết thúc
    CREATE TABLE #TempOrders (
        OrderId     INT,
        CustomerId  INT,
        TotalAmount DECIMAL(18,2),
        INDEX IX_Temp_CustomerId (CustomerId) -- Có thể thêm index!
    );

    INSERT INTO #TempOrders (OrderId, CustomerId, TotalAmount)
    SELECT OrderId, CustomerId, TotalAmount
    FROM Orders
    WHERE Status = 'Pending' AND ProcessedFlag = 0;

    -- GLOBAL temp table: visible cho tất cả sessions
    -- Dùng khi cần share data giữa các sessions/connections
    -- CREATE TABLE ##GlobalTemp (...)  -- Hiếm khi dùng

    -- Xử lý từng batch
    WHILE EXISTS (SELECT 1 FROM #TempOrders WHERE ProcessedFlag IS NULL)
    BEGIN
        UPDATE TOP (@BatchSize) #TempOrders
        SET ProcessedFlag = 1
        OUTPUT inserted.OrderId, inserted.TotalAmount
        INTO OrderProcessingLog (OrderId, Amount)
        WHERE ProcessedFlag IS NULL;
    END;

    -- Explicit drop (optional, sẽ tự drop khi SP kết thúc)
    DROP TABLE IF EXISTS #TempOrders;
END;

Temp Table vs Table Variable

-- Table Variable: nhỏ, no statistics, ít log overhead
DECLARE @OrderSummary TABLE (
    CustomerId  INT,
    OrderCount  INT,
    TotalAmount DECIMAL(18,2)
);

INSERT INTO @OrderSummary
SELECT CustomerId, COUNT(*), SUM(TotalAmount)
FROM Orders
GROUP BY CustomerId;

-- Temp Table: lớn, có statistics, optimizer dùng được
CREATE TABLE #OrderSummary (
    CustomerId  INT PRIMARY KEY,
    OrderCount  INT,
    TotalAmount DECIMAL(18,2)
);

-- Dùng Temp Table khi:
-- 1. Dataset lớn (> vài nghìn rows)
-- 2. Cần index phức tạp
-- 3. Cần thống kê (statistics) để optimizer plan tốt
-- 4. Reuse trong nhiều queries

6. Error Handling

TRY…CATCH

CREATE PROCEDURE dbo.TransferFunds
    @FromAccountId  INT,
    @ToAccountId    INT,
    @Amount         DECIMAL(18,2)
AS
BEGIN
    SET NOCOUNT ON;

    IF @Amount <= 0
        THROW 50001, 'Số tiền chuyển phải lớn hơn 0', 1;

    BEGIN TRY
        BEGIN TRANSACTION;

        -- Kiểm tra số dư
        DECLARE @Balance DECIMAL(18,2);
        SELECT @Balance = Balance FROM Accounts WHERE AccountId = @FromAccountId;

        IF @Balance < @Amount
            THROW 50002, 'Số dư không đủ để thực hiện giao dịch', 1;

        -- Thực hiện transfer
        UPDATE Accounts SET Balance = Balance - @Amount WHERE AccountId = @FromAccountId;
        UPDATE Accounts SET Balance = Balance + @Amount WHERE AccountId = @ToAccountId;

        -- Ghi log
        INSERT INTO TransactionLog (FromAccount, ToAccount, Amount, TransactionDate)
        VALUES (@FromAccountId, @ToAccountId, @Amount, GETDATEUTCDATE());

        COMMIT TRANSACTION;
        PRINT 'Transfer thành công';

    END TRY
    BEGIN CATCH
        IF @@TRANCOUNT > 0
            ROLLBACK TRANSACTION;

        -- Lấy thông tin lỗi
        DECLARE 
            @ErrorNumber    INT = ERROR_NUMBER(),
            @ErrorMessage   NVARCHAR(4000) = ERROR_MESSAGE(),
            @ErrorSeverity  INT = ERROR_SEVERITY(),
            @ErrorState     INT = ERROR_STATE(),
            @ErrorLine      INT = ERROR_LINE(),
            @ErrorProc      NVARCHAR(200) = ERROR_PROCEDURE();

        -- Ghi log lỗi
        INSERT INTO ErrorLog (ErrorNumber, ErrorMessage, ErrorProcedure, ErrorLine, CreatedAt)
        VALUES (@ErrorNumber, @ErrorMessage, @ErrorProc, @ErrorLine, GETDATE());

        -- Re-throw lỗi cho caller
        THROW;
    END CATCH;
END;

ERROR_ Functions

-- Các function lấy thông tin lỗi trong CATCH block
BEGIN CATCH
    SELECT
        ERROR_NUMBER()    AS ErrorNumber,    -- Số hiệu lỗi
        ERROR_MESSAGE()   AS ErrorMessage,   -- Thông điệp lỗi
        ERROR_SEVERITY()  AS ErrorSeverity,  -- Mức độ nghiêm trọng (1-25)
        ERROR_STATE()     AS ErrorState,     -- State code
        ERROR_LINE()      AS ErrorLine,      -- Dòng code gây lỗi
        ERROR_PROCEDURE() AS ErrorProcedure; -- Tên SP/trigger gây lỗi
END CATCH;

RAISERROR vs THROW

-- RAISERROR (cũ, nhưng vẫn dùng)
RAISERROR('Lỗi xử lý đơn hàng %d', 16, 1, @OrderId);
-- %d = int placeholder, %s = string placeholder

-- THROW (SQL 2012+, khuyến cáo dùng)
THROW 50001, 'Lỗi xử lý đơn hàng', 1;

-- Re-throw trong CATCH (không tham số)
BEGIN CATCH
    ROLLBACK TRANSACTION;
    THROW; -- Giữ nguyên error number, message, state gốc
END CATCH;

7. Nested Stored Procedures

-- SP cấp 1: Outer
CREATE PROCEDURE dbo.ProcessDailyBatch
AS
BEGIN
    SET NOCOUNT ON;
    PRINT 'NESTLEVEL: ' + CAST(@@NESTLEVEL AS VARCHAR); -- 1

    -- Gọi SP khác
    EXEC dbo.ValidateOrders;
    EXEC dbo.UpdateInventory;
    EXEC dbo.SendNotifications;
END;

-- SP cấp 2: Inner
CREATE PROCEDURE dbo.ValidateOrders
AS
BEGIN
    SET NOCOUNT ON;
    PRINT 'NESTLEVEL: ' + CAST(@@NESTLEVEL AS VARCHAR); -- 2

    EXEC dbo.CheckOrderRules; -- Level 3
END;

-- Giới hạn: tối đa 32 levels
-- @@NESTLEVEL = 0 khi gọi trực tiếp từ client

8. Transactions trong Stored Procedures

CREATE PROCEDURE dbo.PlaceOrder
    @CustomerId     INT,
    @Items          OrderItemType READONLY, -- Table-valued parameter
    @OrderId        INT OUTPUT
AS
BEGIN
    SET NOCOUNT ON;
    SET XACT_ABORT ON; -- Tự động rollback khi có lỗi runtime

    BEGIN TRY
        BEGIN TRANSACTION;

        -- Tạo Order
        INSERT INTO Orders (CustomerId, OrderDate, Status)
        VALUES (@CustomerId, GETDATE(), 'Pending');

        SET @OrderId = SCOPE_IDENTITY();

        -- Insert Order Items
        INSERT INTO OrderItems (OrderId, ProductId, Quantity, UnitPrice)
        SELECT @OrderId, ProductId, Quantity, UnitPrice
        FROM @Items;

        -- Cập nhật stock
        UPDATE p
        SET p.Stock = p.Stock - i.Quantity
        FROM Products p
        INNER JOIN @Items i ON p.ProductId = i.ProductId;

        -- Kiểm tra stock âm
        IF EXISTS (SELECT 1 FROM Products p INNER JOIN @Items i ON p.ProductId = i.ProductId WHERE p.Stock < 0)
            THROW 50003, 'Sản phẩm không đủ hàng trong kho', 1;

        COMMIT TRANSACTION;

    END TRY
    BEGIN CATCH
        IF @@TRANCOUNT > 0
            ROLLBACK TRANSACTION;
        THROW;
    END CATCH;
END;

@@TRANCOUNT và SAVE TRANSACTION

CREATE PROCEDURE dbo.UpdateWithSavepoint
AS
BEGIN
    -- @@TRANCOUNT: số transaction đang active
    PRINT '@@TRANCOUNT trước: ' + CAST(@@TRANCOUNT AS VARCHAR); -- 0

    BEGIN TRANSACTION; -- @@TRANCOUNT = 1

    INSERT INTO AuditLog (Event) VALUES ('Process started');

    SAVE TRANSACTION MySavepoint; -- Tạo savepoint

    BEGIN TRY
        -- Thao tác có thể lỗi
        INSERT INTO Orders (CustomerId, TotalAmount) VALUES (999999, -100);
    END TRY
    BEGIN CATCH
        -- Rollback về savepoint, giữ lại INSERT AuditLog
        ROLLBACK TRANSACTION MySavepoint;
        INSERT INTO AuditLog (Event) VALUES ('Order insert failed: ' + ERROR_MESSAGE());
    END CATCH;

    COMMIT TRANSACTION; -- Commit AuditLog inserts
END;

9. Dynamic SQL

sp_executesql (Khuyến cáo)

CREATE PROCEDURE dbo.SearchProducts
    @SearchTerm     NVARCHAR(100),
    @CategoryId     INT = NULL,
    @SortColumn     NVARCHAR(50) = 'ProductName',
    @SortDirection  NVARCHAR(4) = 'ASC'
AS
BEGIN
    SET NOCOUNT ON;

    -- Whitelist cho sort columns (ngăn SQL injection)
    IF @SortColumn NOT IN ('ProductName', 'Price', 'CreatedDate', 'Stock')
        SET @SortColumn = 'ProductName';

    IF @SortDirection NOT IN ('ASC', 'DESC')
        SET @SortDirection = 'ASC';

    DECLARE @SQL NVARCHAR(MAX);
    DECLARE @Params NVARCHAR(500);

    SET @SQL = N'
        SELECT ProductId, ProductName, Price, CategoryId
        FROM Products
        WHERE 
            (ProductName LIKE @SearchTerm OR Description LIKE @SearchTerm)
            AND (@CategoryId IS NULL OR CategoryId = @CategoryId)
            AND IsActive = 1
        ORDER BY ' + QUOTENAME(@SortColumn) + ' ' + @SortDirection; -- QUOTENAME bảo vệ tên column

    SET @Params = N'@SearchTerm NVARCHAR(100), @CategoryId INT';

    EXEC sp_executesql 
        @SQL, 
        @Params,
        @SearchTerm = '%' + @SearchTerm + '%',
        @CategoryId = @CategoryId;
END;

Nguy hiểm SQL Injection với EXEC()

-- ĐỪNG làm thế này - dễ bị SQL injection
DECLARE @SQL NVARCHAR(MAX);
SET @SQL = 'SELECT * FROM Products WHERE Name = ''' + @UserInput + '''';
EXEC (@SQL);
-- Nếu @UserInput = "'; DROP TABLE Products; --" thì GG!

-- ĐÚNG: Dùng sp_executesql với parameters
SET @SQL = N'SELECT * FROM Products WHERE Name = @Name';
EXEC sp_executesql @SQL, N'@Name NVARCHAR(200)', @Name = @UserInput;

10. Recompilation và Parameter Sniffing

Vấn đề Parameter Sniffing

-- SP này có vấn đề parameter sniffing
CREATE PROCEDURE dbo.GetOrders @CustomerId INT
AS
    SELECT * FROM Orders WHERE CustomerId = @CustomerId;
-- Nếu compile với CustomerId = 1 (ít orders) → Index Seek plan
-- Khi gọi với CustomerId = 999 (nhiều orders) → vẫn dùng Seek plan → chậm!

Các giải pháp

-- Giải pháp 1: OPTION (RECOMPILE) trên query
-- Recompile query này mỗi lần, tốt cho queries không consistent
CREATE PROCEDURE dbo.GetOrders @CustomerId INT
AS
    SELECT * FROM Orders 
    WHERE CustomerId = @CustomerId
    OPTION (RECOMPILE); -- Recompile mỗi lần, overhead nhỏ

-- Giải pháp 2: WITH RECOMPILE trên SP
-- Recompile toàn bộ SP mỗi lần
CREATE PROCEDURE dbo.GetOrders @CustomerId INT
WITH RECOMPILE -- Không cache plan
AS
    SELECT * FROM Orders WHERE CustomerId = @CustomerId;

-- Giải pháp 3: Local variable (ngăn sniffing)
-- Optimizer không biết giá trị → dùng statistics thay vì parameter value
CREATE PROCEDURE dbo.GetOrders @CustomerId INT
AS
    DECLARE @LocalId INT = @CustomerId;
    SELECT * FROM Orders WHERE CustomerId = @LocalId;

-- Giải pháp 4: OPTIMIZE FOR hint
CREATE PROCEDURE dbo.GetOrders @CustomerId INT
AS
    SELECT * FROM Orders 
    WHERE CustomerId = @CustomerId
    OPTION (OPTIMIZE FOR (@CustomerId = 100)); -- Optimize cho giá trị điển hình

-- Giải pháp 5: OPTIMIZE FOR UNKNOWN
CREATE PROCEDURE dbo.GetOrders @CustomerId INT
AS
    SELECT * FROM Orders 
    WHERE CustomerId = @CustomerId
    OPTION (OPTIMIZE FOR (@CustomerId UNKNOWN)); -- Dùng statistics thay vì parameter value

sp_recompile

-- Force recompile SP lần sau khi gọi
EXEC sp_recompile 'dbo.GetOrders';

-- Force recompile tất cả SPs dùng bảng này
EXEC sp_recompile 'dbo.Orders';

11. System Stored Procedures

-- Thông tin về object
EXEC sp_help 'Orders';              -- Thông tin table/view
EXEC sp_helpindex 'Orders';         -- Danh sách indexes
EXEC sp_helpconstraint 'Orders';    -- Constraints

-- Monitoring
EXEC sp_who2;                       -- Các connections hiện tại
EXEC sp_lock;                       -- Locks đang active

-- Dynamic SQL (đã nói ở trên)
EXEC sp_executesql @SQL, @Params;

-- Thống kê
EXEC sp_updatestats;                -- Update tất cả statistics

-- Metadata
SELECT 
    name, 
    OBJECT_DEFINITION(object_id) AS Definition,
    create_date,
    modify_date
FROM sys.procedures
WHERE name LIKE 'GetOrder%';

-- Tìm SP nào reference đến một table
SELECT DISTINCT 
    o.name AS ProcedureName
FROM sys.sql_modules m
INNER JOIN sys.objects o ON m.object_id = o.object_id
WHERE 
    o.type = 'P'
    AND m.definition LIKE '%Orders%';

Best Practices Tóm Tắt

-- Template cho một Stored Procedure chuẩn
CREATE PROCEDURE dbo.MyProcedure
    @Param1 INT,
    @Param2 NVARCHAR(100) = NULL
AS
BEGIN
    SET NOCOUNT ON;      -- Tắt "X rows affected"
    SET XACT_ABORT ON;   -- Auto rollback khi lỗi

    -- Validate inputs
    IF @Param1 IS NULL OR @Param1 <= 0
        THROW 50001, 'Param1 không hợp lệ', 1;

    BEGIN TRY
        BEGIN TRANSACTION;

        -- Business logic ở đây

        COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        IF @@TRANCOUNT > 0
            ROLLBACK TRANSACTION;

        -- Log error
        INSERT INTO dbo.ErrorLog (ErrorMessage, ErrorProcedure, ErrorLine, CreatedAt)
        VALUES (ERROR_MESSAGE(), ERROR_PROCEDURE(), ERROR_LINE(), GETDATE());

        THROW; -- Re-throw cho caller
    END CATCH;
END;
GO
Best PracticeLý do
SET NOCOUNT ONGiảm network traffic
SET XACT_ABORT ONAuto rollback an toàn
Validate parameters đầu vàoFail fast với thông báo rõ ràng
Dùng schema prefix (dbo.)Tránh resolution ambiguity
Dùng sp_executesql cho dynamic SQLNgăn SQL injection, plan reuse
Tránh SELECT *Explicit columns tốt hơn
Dùng THROW thay RAISERRORModern, giữ context tốt hơn
Comment business logic phức tạpMaintainability

User-Defined Functions (UDFs)

User-Defined Functions (UDFs) trong SQL Server cho phép đóng gói logic tái sử dụng, trả về giá trị đơn hoặc tập kết quả dạng bảng. Khác với Stored Procedures, functions có thể được dùng trực tiếp trong câu SELECT, WHERE, và JOIN.

1. Loại Functions

Tổng quan

User-Defined Functions
├── Scalar Functions          → Trả về một giá trị đơn
├── Table-Valued Functions
│   ├── Inline TVF (iTVF)    → Một câu SELECT duy nhất
│   └── Multi-Statement TVF  → Nhiều câu lệnh, explicit table variable
└── CLR Aggregate Functions  → Tùy chỉnh aggregate (cần CLR)

Scalar Functions

-- Syntax cơ bản
CREATE FUNCTION dbo.CalculateAge
(
    @BirthDate DATE
)
RETURNS INT -- Trả về một giá trị đơn
AS
BEGIN
    DECLARE @Age INT;
    
    SET @Age = DATEDIFF(YEAR, @BirthDate, GETDATE())
        - CASE 
            WHEN MONTH(@BirthDate) > MONTH(GETDATE())
              OR (MONTH(@BirthDate) = MONTH(GETDATE()) AND DAY(@BirthDate) > DAY(GETDATE()))
            THEN 1 
            ELSE 0 
          END;
    
    RETURN @Age;
END;
GO

-- Sử dụng Scalar Function trong SELECT
SELECT 
    EmployeeId,
    FullName,
    BirthDate,
    dbo.CalculateAge(BirthDate) AS Age
FROM Employees;

-- Trong WHERE clause
SELECT * FROM Employees
WHERE dbo.CalculateAge(BirthDate) >= 30;

Inline Table-Valued Functions (iTVF)

-- Inline TVF: Chỉ một câu SELECT, không có BEGIN...END
CREATE FUNCTION dbo.GetOrdersByCustomer
(
    @CustomerId INT,
    @MinAmount  DECIMAL(18,2) = 0
)
RETURNS TABLE -- Không chỉ định cấu trúc table
AS
RETURN
(
    SELECT 
        o.OrderId,
        o.OrderDate,
        o.TotalAmount,
        o.Status,
        c.CustomerName
    FROM Orders o
    INNER JOIN Customers c ON o.CustomerId = c.CustomerId
    WHERE 
        o.CustomerId = @CustomerId
        AND o.TotalAmount >= @MinAmount
);
GO

-- Sử dụng iTVF: dùng như view có tham số
SELECT * FROM dbo.GetOrdersByCustomer(5, 100.00);

-- JOIN với iTVF (CROSS APPLY)
SELECT 
    c.CustomerId,
    c.CustomerName,
    o.OrderId,
    o.TotalAmount
FROM Customers c
CROSS APPLY dbo.GetOrdersByCustomer(c.CustomerId, 0) o
WHERE c.IsActive = 1;

-- OUTER APPLY: giữ lại customers không có orders
SELECT 
    c.CustomerId,
    c.CustomerName,
    o.OrderId,
    o.TotalAmount
FROM Customers c
OUTER APPLY dbo.GetOrdersByCustomer(c.CustomerId, 0) o;

Multi-Statement Table-Valued Functions (msTVF)

-- msTVF: Có BEGIN...END, định nghĩa rõ ràng cấu trúc return table
CREATE FUNCTION dbo.GetEmployeeHierarchy
(
    @ManagerId INT
)
RETURNS @Result TABLE -- Khai báo return table
(
    EmployeeId  INT,
    FullName    NVARCHAR(200),
    Level       INT,
    ManagerId   INT
)
AS
BEGIN
    -- Có thể có nhiều câu lệnh
    INSERT INTO @Result (EmployeeId, FullName, Level, ManagerId)
    VALUES (@ManagerId, (SELECT FullName FROM Employees WHERE EmployeeId = @ManagerId), 0, NULL);

    DECLARE @CurrentLevel INT = 0;

    WHILE EXISTS (
        SELECT 1 FROM Employees e
        INNER JOIN @Result r ON e.ManagerId = r.EmployeeId
        WHERE r.Level = @CurrentLevel
        AND e.EmployeeId NOT IN (SELECT EmployeeId FROM @Result)
    )
    BEGIN
        INSERT INTO @Result (EmployeeId, FullName, Level, ManagerId)
        SELECT 
            e.EmployeeId,
            e.FullName,
            @CurrentLevel + 1,
            e.ManagerId
        FROM Employees e
        INNER JOIN @Result r ON e.ManagerId = r.EmployeeId
        WHERE r.Level = @CurrentLevel
        AND e.EmployeeId NOT IN (SELECT EmployeeId FROM @Result);

        SET @CurrentLevel = @CurrentLevel + 1;
    END;

    RETURN; -- Không cần return value, data đã trong @Result
END;
GO

-- Sử dụng msTVF
SELECT EmployeeId, FullName, Level
FROM dbo.GetEmployeeHierarchy(1)
ORDER BY Level, FullName;

2. So Sánh iTVF vs msTVF: Performance

Tiêu chíInline TVF (iTVF)Multi-Statement TVF (msTVF)
Cú phápMột SELECT, không BEGIN/ENDCó BEGIN/END, khai báo @Table
OptimizerCó thể “inline” (mở rộng như view)Black box - optimizer không nhìn thấy bên trong
StatisticsDùng base table statisticsKhông có statistics trên @Table variable
ParallelismĐượcKhông được (trước SQL 2022)
ComplexityĐơn giảnPhức tạp hơn
Use caseMọi khi có thểKhi cần nhiều câu lệnh/vòng lặp
-- DEMO: So sánh execution plan

-- iTVF: Optimizer có thể thấy bên trong và optimize
CREATE FUNCTION dbo.GetActiveOrders_iTVF(@Status NVARCHAR(50))
RETURNS TABLE AS RETURN
(
    SELECT OrderId, CustomerId, TotalAmount
    FROM Orders
    WHERE Status = @Status
);

-- msTVF: Optimizer không nhìn thấy bên trong
CREATE FUNCTION dbo.GetActiveOrders_msTVF(@Status NVARCHAR(50))
RETURNS @T TABLE (OrderId INT, CustomerId INT, TotalAmount DECIMAL(18,2))
AS
BEGIN
    INSERT @T
    SELECT OrderId, CustomerId, TotalAmount
    FROM Orders
    WHERE Status = @Status;
    RETURN;
END;

-- Query với JOIN - iTVF sẽ tốt hơn vì optimizer có thể push predicate vào function
SELECT c.CustomerName, o.TotalAmount
FROM Customers c
CROSS APPLY dbo.GetActiveOrders_iTVF('Shipped') o  -- Better
WHERE c.CustomerId = o.CustomerId
  AND c.Region = 'North';

Kết luận: Luôn ưu tiên iTVF khi có thể.


3. Deterministic vs Non-Deterministic Functions

-- DETERMINISTIC: Cùng input → luôn cùng output
-- Ví dụ: UPPER(), LOWER(), LEN(), DATEADD(), SQRT()

-- NON-DETERMINISTIC: Cùng input có thể khác output
-- Ví dụ: GETDATE(), RAND(), NEWID(), GETUTCDATE()

-- Tại sao quan trọng?
-- 1. Indexed columns/computed columns chỉ chấp nhận deterministic functions
-- 2. Indexed views yêu cầu deterministic
-- 3. WITH SCHEMABINDING function được đánh dấu deterministic nếu eligible

-- UDF deterministic khi:
-- - Sử dụng WITH SCHEMABINDING
-- - Chỉ gọi deterministic built-in functions
-- - Không access database objects (bảng, views...)

CREATE FUNCTION dbo.FormatCurrency
(
    @Amount DECIMAL(18,2),
    @CurrencyCode CHAR(3)
)
RETURNS NVARCHAR(50)
WITH SCHEMABINDING -- Cần để mark deterministic
AS
BEGIN
    RETURN @CurrencyCode + ' ' + FORMAT(@Amount, 'N2');
END;
GO

-- Kiểm tra function có deterministic không
SELECT 
    name, 
    is_deterministic 
FROM sys.objects o
JOIN sys.sql_modules m ON o.object_id = m.object_id
WHERE o.type IN ('FN', 'TF', 'IF');

-- Dùng deterministic function trong computed column
ALTER TABLE Products
ADD FormattedPrice AS dbo.FormatCurrency(Price, 'USD') PERSISTED;
-- PERSISTED: lưu vật lý, cập nhật khi Price thay đổi

4. Function Restrictions (Những gì KHÔNG được làm)

-- Functions KHÔNG được phép:
-- 1. INSERT/UPDATE/DELETE trên real tables
-- 2. CREATE/DROP tables (ngoại trừ table variables)
-- 3. Gọi Stored Procedures
-- 4. TRY...CATCH (có thể dùng trong msTVF nhưng hạn chế)
-- 5. Dynamic SQL với EXEC() (sp_executesql với SELECT được phép trong một số trường hợp)
-- 6. Thay đổi state database (SET options, COMMIT/ROLLBACK cấp cao)

-- VI DỤ LỖI:
CREATE FUNCTION dbo.BadFunction(@Id INT)
RETURNS INT
AS
BEGIN
    -- LỖI: Không thể INSERT vào real table trong function
    INSERT INTO AuditLog (Action) VALUES ('Called'); -- ERROR!
    
    RETURN 1;
END;

-- GIẢI PHÁP: Dùng Stored Procedure thay vì Function khi cần side effects

5. Performance Caveat của Scalar UDFs

Vấn đề Row-by-Row Execution

-- Scalar UDF chạy row-by-row, KHÔNG thể parallelize (trước SQL 2019)
CREATE FUNCTION dbo.GetCustomerTier(@CustomerId INT)
RETURNS NVARCHAR(50)
AS
BEGIN
    DECLARE @TotalSpent DECIMAL(18,2);
    
    SELECT @TotalSpent = SUM(TotalAmount)
    FROM Orders
    WHERE CustomerId = @CustomerId;
    
    RETURN CASE 
        WHEN @TotalSpent >= 10000 THEN 'Platinum'
        WHEN @TotalSpent >= 5000  THEN 'Gold'
        WHEN @TotalSpent >= 1000  THEN 'Silver'
        ELSE 'Bronze'
    END;
END;
GO

-- Query này sẽ gọi function 1 TRIỆU lần nếu có 1 triệu customers!
-- Serial execution, rất chậm
SELECT 
    CustomerId,
    CustomerName,
    dbo.GetCustomerTier(CustomerId) AS Tier -- Row-by-row!
FROM Customers;

-- GIẢI PHÁP: Viết set-based thay thế
SELECT
    CustomerId,
    CustomerName,
    CASE 
        WHEN SUM(o.TotalAmount) >= 10000 THEN 'Platinum'
        WHEN SUM(o.TotalAmount) >= 5000  THEN 'Gold'
        WHEN SUM(o.TotalAmount) >= 1000  THEN 'Silver'
        ELSE 'Bronze'
    END AS Tier
FROM Customers c
LEFT JOIN Orders o ON c.CustomerId = o.CustomerId
GROUP BY c.CustomerId, c.CustomerName;

6. Scalar UDF Inlining trong SQL Server 2019+

-- SQL Server 2019 tự động inline eligible scalar UDFs
-- Biến scalar UDF thành relational expression, cho phép:
-- - Parallelism
-- - Cost-based optimization  
-- - Predicate pushdown

-- Điều kiện để được inline tự động:
-- ✅ Chỉ một RETURN statement
-- ✅ Không gọi EXECUTE
-- ✅ Không đệ quy
-- ✅ Không dùng TRY...CATCH
-- ✅ Không dùng table variables (ngoại trừ trong subquery)
-- ✅ Không có side effects
-- ❌ Không gọi RAND(), NEWID() (non-deterministic)

-- Kiểm tra function có được inline không
SELECT 
    o.name AS FunctionName,
    m.is_inlineable
FROM sys.sql_modules m
JOIN sys.objects o ON m.object_id = o.object_id
WHERE o.type = 'FN'; -- Scalar functions

-- Force inline cho một query cụ thể
SELECT 
    CustomerId,
    dbo.GetCustomerTier(CustomerId) AS Tier
FROM Customers
OPTION (USE HINT('ENABLE_TSQL_SCALAR_UDF_INLINING')); -- Force enable

-- Disable inline nếu gây vấn đề
SELECT 
    CustomerId,
    dbo.GetCustomerTier(CustomerId) WITH (INLINE = OFF) AS Tier -- SQL 2019+
FROM Customers;

-- Database level control
ALTER DATABASE MyDatabase
SET TSQL_SCALAR_UDF_INLINING = ON; -- Default ON trong compatibility 150+

7. Rewriting Scalar UDFs như iTVFs (Performance Pattern)

Đây là pattern quan trọng nhất để cải thiện hiệu năng function.

-- BEFORE: Scalar UDF - chậm, không parallel
CREATE FUNCTION dbo.GetProductDiscount_Scalar(@ProductId INT, @Quantity INT)
RETURNS DECIMAL(5,2)
AS
BEGIN
    DECLARE @Discount DECIMAL(5,2) = 0;
    DECLARE @Category NVARCHAR(50);
    
    SELECT @Category = CategoryName
    FROM Products p
    JOIN Categories c ON p.CategoryId = c.CategoryId
    WHERE p.ProductId = @ProductId;
    
    IF @Category = 'Electronics' AND @Quantity >= 10
        SET @Discount = 0.15;
    ELSE IF @Quantity >= 20
        SET @Discount = 0.10;
    ELSE IF @Quantity >= 5
        SET @Discount = 0.05;
    
    RETURN @Discount;
END;
GO

-- AFTER: Inline TVF - nhanh, optimizer can see inside
CREATE FUNCTION dbo.GetProductDiscount_iTVF(@ProductId INT, @Quantity INT)
RETURNS TABLE
AS
RETURN
(
    SELECT 
        CASE 
            WHEN c.CategoryName = 'Electronics' AND @Quantity >= 10 THEN 0.15
            WHEN @Quantity >= 20 THEN 0.10
            WHEN @Quantity >= 5  THEN 0.05
            ELSE 0.00
        END AS Discount
    FROM Products p
    JOIN Categories c ON p.CategoryId = c.CategoryId
    WHERE p.ProductId = @ProductId
);
GO

-- Sử dụng iTVF với CROSS APPLY
SELECT 
    od.OrderDetailId,
    od.ProductId,
    od.Quantity,
    d.Discount,
    od.UnitPrice * od.Quantity * (1 - d.Discount) AS FinalAmount
FROM OrderDetails od
CROSS APPLY dbo.GetProductDiscount_iTVF(od.ProductId, od.Quantity) d;

8. CLR Aggregate Functions

-- CLR Aggregate cần code C# và assembly registration
-- Ví dụ use case: Concatenate strings (như STRING_AGG trong SQL 2017+)

-- Sau khi deploy CLR assembly:
-- CREATE AGGREGATE dbo.StringConcat(@value NVARCHAR(MAX), @delimiter NVARCHAR(10))
-- RETURNS NVARCHAR(MAX)
-- EXTERNAL NAME MyAssembly.[MyNamespace.StringConcatAggregate];

-- Trong SQL Server 2017+, dùng STRING_AGG thay vì CLR:
SELECT 
    DepartmentId,
    STRING_AGG(FullName, ', ') WITHIN GROUP (ORDER BY FullName) AS EmployeeNames
FROM Employees
GROUP BY DepartmentId;

9. System Functions Overview

Metadata Functions

-- OBJECT_ID: Lấy object_id từ tên
SELECT OBJECT_ID('dbo.Orders');          -- Trả về INT hoặc NULL
SELECT OBJECT_ID('dbo.Orders', 'U');     -- 'U' = User table

-- OBJECT_NAME: Ngược lại OBJECT_ID
SELECT OBJECT_NAME(OBJECT_ID('dbo.Orders'));

-- SCHEMA_NAME: Lấy tên schema từ schema_id
SELECT SCHEMA_NAME(1); -- 'dbo'

-- Kiểm tra tồn tại trước khi create/drop
IF OBJECT_ID('dbo.MyFunction', 'FN') IS NOT NULL
    DROP FUNCTION dbo.MyFunction;
GO
CREATE FUNCTION dbo.MyFunction...

-- SQL 2016+: DROP IF EXISTS
DROP FUNCTION IF EXISTS dbo.MyFunction;

-- COLUMNPROPERTY: Thông tin về column
SELECT COLUMNPROPERTY(OBJECT_ID('Orders'), 'TotalAmount', 'Precision');

-- INDEX_COL: Tên column trong index
SELECT INDEX_COL('dbo.Orders', 1, 1); -- (table, index_id, key_ordinal)

Security Functions

-- USER_NAME(): Tên user hiện tại trong database
SELECT USER_NAME(); -- 'dbo', 'john', etc.

-- SYSTEM_USER: Login name
SELECT SYSTEM_USER; -- 'DOMAIN\john'

-- IS_MEMBER: Kiểm tra membership trong role
SELECT IS_MEMBER('db_datareader'); -- 1 = Yes, 0 = No, NULL = lỗi

-- IS_ROLEMEMBER: Tương tự nhưng rõ ràng hơn
SELECT IS_ROLEMEMBER('db_owner', 'john_user');

-- HAS_PERMS_BY_NAME: Kiểm tra quyền
SELECT HAS_PERMS_BY_NAME('dbo.Orders', 'OBJECT', 'SELECT'); -- 1 nếu có quyền

-- Trong Function để dynamic security
CREATE FUNCTION dbo.GetSensitiveData(@RequestUserId INT)
RETURNS TABLE
AS
RETURN
(
    SELECT *
    FROM SensitiveTable
    WHERE 
        IS_MEMBER('SensitiveDataViewers') = 1
        OR RequestedBy = @RequestUserId
);

10. Ví dụ So Sánh Toàn Diện

-- SCENARIO: Lấy discount percentage cho orderdetail

-- ❌ OPTION 1: Scalar UDF (tệ nhất)
CREATE FUNCTION dbo.GetDiscount_Scalar(@ProductId INT, @Quantity INT)
RETURNS DECIMAL(5,2)
AS
BEGIN
    DECLARE @Discount DECIMAL(5,2);
    SELECT TOP 1 @Discount = DiscountRate
    FROM DiscountRules
    WHERE ProductId = @ProductId AND MinQuantity <= @Quantity
    ORDER BY MinQuantity DESC;
    RETURN ISNULL(@Discount, 0);
END;

SELECT od.*, dbo.GetDiscount_Scalar(od.ProductId, od.Quantity) AS Disc
FROM OrderDetails od; -- N lần gọi function = N lần query DiscountRules

-- ✅ OPTION 2: Inline TVF (tốt)
CREATE FUNCTION dbo.GetDiscount_iTVF(@ProductId INT, @Quantity INT)
RETURNS TABLE AS RETURN
(
    SELECT TOP 1 DiscountRate AS Discount
    FROM DiscountRules
    WHERE ProductId = @ProductId AND MinQuantity <= @Quantity
    ORDER BY MinQuantity DESC
);

SELECT od.*, d.Discount
FROM OrderDetails od
CROSS APPLY dbo.GetDiscount_iTVF(od.ProductId, od.Quantity) d;
-- Optimizer có thể join và optimize toàn bộ query!

-- ✅ OPTION 3: Set-based JOIN (tốt nhất)
SELECT 
    od.*,
    ISNULL(dr.DiscountRate, 0) AS Discount
FROM OrderDetails od
OUTER APPLY (
    SELECT TOP 1 DiscountRate
    FROM DiscountRules dr
    WHERE dr.ProductId = od.ProductId AND dr.MinQuantity <= od.Quantity
    ORDER BY dr.MinQuantity DESC
) dr;

Checklist Khi Viết Functions

Câu hỏiGợi ý
Cần trả về một giá trị hay nhiều rows?Scalar vs TVF
Logic có thể biểu diễn trong 1 SELECT không?iTVF (ưu tiên)
Cần nhiều bước xử lý?msTVF hoặc SP
Function sẽ gọi trong SELECT từng row?Cân nhắc rewrite thành set-based
SQL Server 2019+ và UDF đơn giản?Scalar UDF có thể được inline
Cần dùng trong computed column?Phải deterministic + SCHEMABINDING
Cần side effects (ghi log, update)?Dùng Stored Procedure, không dùng Function

Triggers

Trigger là một loại stored procedure đặc biệt tự động thực thi (kích hoạt) khi xảy ra một sự kiện nhất định trên database. Trigger không được gọi trực tiếp - chúng phản ứng với các events.

Cảnh báo: Trigger là công cụ mạnh nhưng dễ lạm dụng. Luôn cân nhắc kỹ trước khi dùng.

1. Loại Triggers

SQL Server Triggers
├── DML Triggers (Data Manipulation Language)
│   ├── AFTER (FOR) Trigger
│   │   ├── AFTER INSERT
│   │   ├── AFTER UPDATE
│   │   └── AFTER DELETE
│   └── INSTEAD OF Trigger
│       ├── INSTEAD OF INSERT
│       ├── INSTEAD OF UPDATE
│       └── INSTEAD OF DELETE
├── DDL Triggers (Data Definition Language)
│   ├── Database-level (CREATE, ALTER, DROP objects)
│   └── Server-level (CREATE DATABASE, etc.)
└── Logon Triggers
    └── LOGON event

2. DML Triggers: inserted và deleted Tables

Khi DML trigger kích hoạt, SQL Server tạo hai bảng ảo (pseudo-tables):

  • inserted: Chứa rows mới (sau INSERT hoặc sau UPDATE)
  • deleted: Chứa rows cũ (trước DELETE hoặc trước UPDATE)
Eventinserteddeleted
INSERTRows được INSERTTrống
DELETETrốngRows bị DELETE
UPDATERows mới (sau update)Rows cũ (trước update)
-- AFTER INSERT Trigger: Audit khi thêm đơn hàng
CREATE TRIGGER trg_Orders_AfterInsert
ON dbo.Orders
AFTER INSERT
AS
BEGIN
    SET NOCOUNT ON;

    INSERT INTO dbo.OrderAuditLog (
        OrderId, 
        CustomerId, 
        TotalAmount, 
        Action, 
        ActionBy, 
        ActionDate
    )
    SELECT 
        i.OrderId,
        i.CustomerId,
        i.TotalAmount,
        'INSERT',
        SYSTEM_USER,
        GETDATE()
    FROM inserted i; -- inserted có thể chứa NHIỀU rows!
END;
GO

-- AFTER DELETE Trigger: Soft delete / archive
CREATE TRIGGER trg_Products_AfterDelete
ON dbo.Products
AFTER DELETE
AS
BEGIN
    SET NOCOUNT ON;

    -- Lưu vào archive trước khi mất
    INSERT INTO dbo.ProductsArchive (
        ProductId, ProductName, Price, CategoryId, DeletedBy, DeletedDate
    )
    SELECT 
        d.ProductId,
        d.ProductName,
        d.Price,
        d.CategoryId,
        SYSTEM_USER,
        GETDATE()
    FROM deleted d;
END;
GO

-- AFTER UPDATE Trigger: Track thay đổi
CREATE TRIGGER trg_Employees_AfterUpdate
ON dbo.Employees
AFTER UPDATE
AS
BEGIN
    SET NOCOUNT ON;

    -- So sánh inserted (mới) và deleted (cũ)
    INSERT INTO dbo.EmployeeChangeLog (
        EmployeeId,
        ColumnChanged,
        OldValue,
        NewValue,
        ChangedBy,
        ChangedDate
    )
    SELECT
        i.EmployeeId,
        'Salary',
        CAST(d.Salary AS NVARCHAR(50)),
        CAST(i.Salary AS NVARCHAR(50)),
        SYSTEM_USER,
        GETDATE()
    FROM inserted i
    INNER JOIN deleted d ON i.EmployeeId = d.EmployeeId
    WHERE i.Salary <> d.Salary; -- Chỉ log khi Salary thực sự thay đổi
END;
GO

3. INSTEAD OF Triggers

INSTEAD OF trigger thay thế (intercept) DML gốc. DML gốc không chạy trừ khi trigger tự thực hiện nó.

INSTEAD OF trên View (Updatable Views)

-- View join nhiều bảng - mặc định không update được
CREATE VIEW dbo.vw_EmployeeDetails
AS
SELECT 
    e.EmployeeId,
    e.FirstName,
    e.LastName,
    e.Salary,
    d.DepartmentName,
    d.DepartmentId
FROM Employees e
INNER JOIN Departments d ON e.DepartmentId = d.DepartmentId;
GO

-- INSTEAD OF INSERT: Cho phép INSERT qua view
CREATE TRIGGER trg_vw_EmployeeDetails_InsteadOfInsert
ON dbo.vw_EmployeeDetails
INSTEAD OF INSERT
AS
BEGIN
    SET NOCOUNT ON;

    -- Validate
    IF EXISTS (SELECT 1 FROM inserted WHERE Salary < 0)
    BEGIN
        THROW 50001, 'Salary không được âm', 1;
    END;

    -- Thực hiện INSERT vào base table
    INSERT INTO dbo.Employees (FirstName, LastName, Salary, DepartmentId)
    SELECT i.FirstName, i.LastName, i.Salary, i.DepartmentId
    FROM inserted i;
END;
GO

-- INSTEAD OF UPDATE: Xử lý update phức tạp
CREATE TRIGGER trg_vw_EmployeeDetails_InsteadOfUpdate
ON dbo.vw_EmployeeDetails
INSTEAD OF UPDATE
AS
BEGIN
    SET NOCOUNT ON;

    -- Chỉ cho phép update các cột employee, không cho phép update department tên
    UPDATE e
    SET 
        e.FirstName = i.FirstName,
        e.LastName  = i.LastName,
        e.Salary    = i.Salary
    FROM Employees e
    INNER JOIN inserted i ON e.EmployeeId = i.EmployeeId;
    
    -- Nếu muốn update DepartmentId, xử lý riêng
    UPDATE e
    SET e.DepartmentId = i.DepartmentId
    FROM Employees e
    INNER JOIN inserted i ON e.EmployeeId = i.EmployeeId
    WHERE e.DepartmentId <> i.DepartmentId;
END;
GO

INSTEAD OF DELETE với Soft Delete

-- Thay vì xóa thật, mark IsDeleted = 1
CREATE TRIGGER trg_Orders_InsteadOfDelete
ON dbo.Orders
INSTEAD OF DELETE
AS
BEGIN
    SET NOCOUNT ON;

    UPDATE o
    SET 
        o.IsDeleted = 1,
        o.DeletedBy = SYSTEM_USER,
        o.DeletedDate = GETDATE()
    FROM dbo.Orders o
    INNER JOIN deleted d ON o.OrderId = d.OrderId;

    -- Không INSERT/DELETE thật → records không bị xóa
END;
GO

4. Trigger Context: @@ROWCOUNT, COLUMNS_UPDATED(), UPDATE()

CREATE TRIGGER trg_OrderDetails_Update
ON dbo.OrderDetails
AFTER UPDATE
AS
BEGIN
    SET NOCOUNT ON;

    -- @@ROWCOUNT: Số rows bị ảnh hưởng
    -- Nếu 0 rows thì không cần xử lý
    IF @@ROWCOUNT = 0 RETURN;

    -- UPDATE(column): TRUE nếu column được include trong UPDATE statement
    -- Lưu ý: TRUE cả khi giá trị không đổi!
    IF UPDATE(Quantity) OR UPDATE(UnitPrice)
    BEGIN
        -- Recalculate LineTotal
        UPDATE od
        SET od.LineTotal = od.Quantity * od.UnitPrice
        FROM dbo.OrderDetails od
        INNER JOIN inserted i ON od.OrderDetailId = i.OrderDetailId;
    END;

    -- COLUMNS_UPDATED(): Bitmask của columns được update
    -- ít dùng vì phụ thuộc vào ordinal position của column
    -- Column 1 = bit 1 (1), Column 2 = bit 2 (2), Column 3 = bit 3 (4)...
    -- COLUMNS_UPDATED() & 4 = 4 → Column 3 được update
    DECLARE @ChangedColumns VARBINARY(10) = COLUMNS_UPDATED();
    IF (@ChangedColumns & 2) = 2 -- Column 2 (Quantity)
    BEGIN
        INSERT INTO AuditLog (TableName, Event, ChangedAt)
        VALUES ('OrderDetails', 'Quantity Changed', GETDATE());
    END;
END;
GO

5. Nested và Recursive Triggers

-- NESTED TRIGGERS: Trigger A fires trigger B
-- Controlled bởi: sp_configure 'nested triggers', 1/0
-- Mặc định: ON (nested triggers enabled)

-- Ví dụ nested: 
-- UPDATE Orders → trg_Orders_Update → UPDATE OrderSummary → trg_OrderSummary_Update

-- Kiểm tra setting
SELECT name, value_in_use
FROM sys.configurations
WHERE name = 'nested triggers';

-- Disable nested triggers
EXEC sp_configure 'nested triggers', 0;
RECONFIGURE;

-- RECURSIVE TRIGGERS: Trigger gọi lại chính nó
-- Controlled bởi: ALTER DATABASE ... SET RECURSIVE_TRIGGERS ON/OFF
-- Mặc định: OFF

ALTER DATABASE MyDatabase SET RECURSIVE_TRIGGERS ON;

-- Ví dụ recursive trigger (CẦN THẬN - dễ gây infinite loop!)
CREATE TRIGGER trg_Categories_Update
ON dbo.Categories
AFTER UPDATE
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Nếu không có guard condition → infinite loop!
    IF UPDATE(ParentCategoryId)
    BEGIN
        -- Update parent's child count
        UPDATE c
        SET c.ChildCount = (SELECT COUNT(*) FROM Categories WHERE ParentCategoryId = c.CategoryId)
        FROM Categories c
        WHERE c.CategoryId IN (SELECT ParentCategoryId FROM inserted WHERE ParentCategoryId IS NOT NULL);
        -- Điều này có thể trigger lại trigger này!
    END;
END;

6. Thứ Tự Trigger: sp_settriggerorder

-- Khi có nhiều AFTER trigger trên cùng một event, 
-- có thể chỉ định trigger nào chạy FIRST và LAST

-- Ví dụ: 3 triggers trên Orders AFTER INSERT
-- trg_Validate → trg_Audit → trg_Notify

EXEC sp_settriggerorder 
    @triggername = 'trg_Orders_Validate',
    @order = 'FIRST',
    @stmttype = 'INSERT';

EXEC sp_settriggerorder 
    @triggername = 'trg_Orders_Notify',
    @order = 'LAST',
    @stmttype = 'INSERT';

-- Triggers ở giữa không có guaranteed order

-- Xem trigger order
SELECT 
    t.name AS TriggerName,
    te.type_desc AS EventType,
    t.is_disabled
FROM sys.triggers t
JOIN sys.trigger_events te ON t.object_id = te.object_id
WHERE t.parent_id = OBJECT_ID('dbo.Orders')
ORDER BY t.name;

7. DDL Triggers: Audit Schema Changes

-- DATABASE-LEVEL DDL Trigger: Khi ai đó thay đổi schema
CREATE TRIGGER trg_DDL_AuditSchemaChanges
ON DATABASE
FOR CREATE_TABLE, ALTER_TABLE, DROP_TABLE,
    CREATE_PROCEDURE, ALTER_PROCEDURE, DROP_PROCEDURE,
    CREATE_VIEW, ALTER_VIEW, DROP_VIEW,
    CREATE_INDEX, DROP_INDEX
AS
BEGIN
    SET NOCOUNT ON;

    -- EVENTDATA(): Trả về XML mô tả sự kiện DDL
    DECLARE @EventData XML = EVENTDATA();

    INSERT INTO dbo.SchemaChangeLog (
        EventType,
        ObjectName,
        ObjectType,
        SchemaName,
        CommandText,
        LoginName,
        HostName,
        EventTime
    )
    SELECT
        @EventData.value('(/EVENT_INSTANCE/EventType)[1]',    'NVARCHAR(100)'),
        @EventData.value('(/EVENT_INSTANCE/ObjectName)[1]',   'NVARCHAR(200)'),
        @EventData.value('(/EVENT_INSTANCE/ObjectType)[1]',   'NVARCHAR(100)'),
        @EventData.value('(/EVENT_INSTANCE/SchemaName)[1]',   'NVARCHAR(100)'),
        @EventData.value('(/EVENT_INSTANCE/TSQLCommand)[1]',  'NVARCHAR(MAX)'),
        @EventData.value('(/EVENT_INSTANCE/LoginName)[1]',    'NVARCHAR(200)'),
        HOST_NAME(),
        GETDATE();
END;
GO

-- SERVER-LEVEL DDL Trigger: Audit database creation
CREATE TRIGGER trg_Server_PreventDropDatabase
ON ALL SERVER
FOR DROP_DATABASE
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @EventData XML = EVENTDATA();
    DECLARE @DbName NVARCHAR(200) = @EventData.value('(/EVENT_INSTANCE/DatabaseName)[1]', 'NVARCHAR(200)');

    -- Chặn xóa production databases
    IF @DbName LIKE 'PROD_%'
    BEGIN
        RAISERROR('Không được phép DROP production database!', 16, 1);
        ROLLBACK; -- Hủy lệnh DROP DATABASE
    END;
END;
GO

8. Performance Impact của Triggers

Tại sao Triggers ảnh hưởng Performance?

  1. Synchronous: Trigger chạy trong cùng transaction với DML gốc
  2. Blocking: DML phải chờ trigger hoàn thành mới commit
  3. inserted/deleted tables: Phải tạo copy của data vào tempdb
  4. Locks: Transaction dài hơn → lock lâu hơn → deadlock potential
  5. Invisible overhead: Developer không thấy trigger khi viết DML
-- Đo hiệu năng trigger
SET STATISTICS TIME ON;
SET STATISTICS IO ON;

-- DML với trigger
UPDATE Orders SET Status = 'Shipped' WHERE OrderId = 1;
-- Xem output để thấy trigger time

-- Disable trigger để so sánh
DISABLE TRIGGER trg_Orders_AfterUpdate ON Orders;
UPDATE Orders SET Status = 'Shipped' WHERE OrderId = 1;
ENABLE TRIGGER trg_Orders_AfterUpdate ON Orders;

9. Trigger Anti-Patterns

Anti-Pattern 1: Giả sử Chỉ Có Một Row

-- ❌ SAI: Giả sử một row mỗi lần
CREATE TRIGGER trg_Bad_SingleRow ON Orders AFTER INSERT
AS
BEGIN
    -- GetDate với SCALAR - chỉ lấy một row!
    DECLARE @OrderId INT = (SELECT OrderId FROM inserted);
    DECLARE @CustomerId INT = (SELECT CustomerId FROM inserted);
    
    -- Nếu INSERT nhiều rows → lỗi hoặc lấy row tùy ý!
    
    EXEC dbo.SendOrderConfirmation @OrderId, @CustomerId;
END;

-- ✅ ĐÚNG: Xử lý multi-row
CREATE TRIGGER trg_Good_MultiRow ON Orders AFTER INSERT
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Xử lý tất cả rows trong inserted
    INSERT INTO NotificationQueue (OrderId, CustomerId, QueuedAt)
    SELECT OrderId, CustomerId, GETDATE()
    FROM inserted;
    -- SP để xử lý từng notification sẽ được gọi bởi job riêng
END;

Anti-Pattern 2: CURSOR trong Trigger

-- ❌ SAI: Cursor trong trigger = rất chậm
CREATE TRIGGER trg_Bad_CursorInTrigger ON Orders AFTER INSERT
AS
BEGIN
    DECLARE @OrderId INT;
    DECLARE cur CURSOR FOR SELECT OrderId FROM inserted;
    OPEN cur;
    FETCH NEXT FROM cur INTO @OrderId;
    WHILE @@FETCH_STATUS = 0
    BEGIN
        EXEC dbo.ProcessOrder @OrderId; -- Gọi SP cho từng row
        FETCH NEXT FROM cur INTO @OrderId;
    END;
    CLOSE cur;
    DEALLOCATE cur;
END;

-- ✅ TỐT HƠN: Batch vào queue, xử lý async
CREATE TRIGGER trg_Good_BatchQueue ON Orders AFTER INSERT
AS
BEGIN
    SET NOCOUNT ON;
    INSERT INTO ProcessingQueue (OrderId, QueuedAt, Status)
    SELECT OrderId, GETDATE(), 'Pending'
    FROM inserted;
    -- Background job sẽ xử lý queue
END;

Anti-Pattern 3: Business Logic Phức Tạp trong Trigger

-- ❌ TRÁNH: Quá nhiều logic trong trigger
-- → Khó debug, khó test, khó maintain
-- → Developer INSERT vào Orders không biết trigger làm gì

-- ✅ THAY BẰNG: Application logic hoặc Stored Procedure rõ ràng

10. Alternatives to Triggers

Computed Columns (thay vì trigger calculate)

-- Thay vì trigger tính LineTotal
-- ❌ Trigger approach
CREATE TRIGGER trg_CalcLineTotal ON OrderDetails AFTER INSERT, UPDATE
AS UPDATE od SET od.LineTotal = od.Quantity * od.UnitPrice
FROM OrderDetails od INNER JOIN inserted i ON od.OrderDetailId = i.OrderDetailId;

-- ✅ Computed Column approach (tự động, không cần trigger)
ALTER TABLE OrderDetails
ADD LineTotal AS (Quantity * UnitPrice) PERSISTED;

Filtered Indexes (thay vì trigger validate)

-- Thay vì trigger ngăn duplicate active orders
-- ✅ Unique filtered index
CREATE UNIQUE INDEX UIX_ActiveOrders_Customer
ON Orders (CustomerId)
WHERE Status = 'Active'; -- Chỉ enforce unique khi Active

CDC (Change Data Capture) thay vì Audit Trigger

-- Enable CDC cho bảng Orders
EXEC sys.sp_cdc_enable_db; -- Enable cho database

EXEC sys.sp_cdc_enable_table
    @source_schema = 'dbo',
    @source_name   = 'Orders',
    @role_name     = NULL, -- Security role
    @capture_instance = 'dbo_Orders',
    @supports_net_changes = 1;

-- Query changes (không cần trigger!)
SELECT 
    __$operation, -- 1=DELETE, 2=INSERT, 3=before UPDATE, 4=after UPDATE
    __$start_lsn,
    OrderId,
    TotalAmount,
    Status
FROM cdc.fn_cdc_get_all_changes_dbo_Orders(
    @from_lsn, @to_lsn, 'all'
);

11. DISABLE / ENABLE TRIGGER

-- Disable một trigger cụ thể
DISABLE TRIGGER trg_Orders_AfterInsert ON dbo.Orders;

-- Enable lại
ENABLE TRIGGER trg_Orders_AfterInsert ON dbo.Orders;

-- Disable TẤT CẢ triggers trên một table
DISABLE TRIGGER ALL ON dbo.Orders;
ENABLE TRIGGER ALL ON dbo.Orders;

-- Disable TẤT CẢ DDL triggers trên database
DISABLE TRIGGER ALL ON DATABASE;
ENABLE TRIGGER ALL ON DATABASE;

-- Use case: Import dữ liệu lớn mà không muốn trigger chạy
DISABLE TRIGGER ALL ON dbo.Orders;
-- BulkInsert...
ENABLE TRIGGER ALL ON dbo.Orders;

-- Kiểm tra trigger nào đang enabled/disabled
SELECT 
    t.name,
    t.is_disabled,
    te.type_desc AS EventType,
    OBJECT_NAME(t.parent_id) AS TableName
FROM sys.triggers t
JOIN sys.trigger_events te ON t.object_id = te.object_id
WHERE t.parent_id = OBJECT_ID('dbo.Orders');

12. Logon Triggers

-- Logon Trigger: Kiểm soát ai được phép login
CREATE TRIGGER trg_LimitConnections
ON ALL SERVER
WITH EXECUTE AS 'sa'
FOR LOGON
AS
BEGIN
    -- Giới hạn số connections của một login
    IF ORIGINAL_LOGIN() = 'ReportUser'
    BEGIN
        IF (
            SELECT COUNT(*) 
            FROM sys.dm_exec_sessions 
            WHERE login_name = 'ReportUser'
        ) > 5
        BEGIN
            RAISERROR('Quá nhiều connections cho ReportUser', 16, 1);
            ROLLBACK; -- Từ chối connection
        END;
    END;

    -- Block connections ngoài giờ làm việc
    IF ORIGINAL_LOGIN() NOT IN ('sa', 'AdminUser')
    BEGIN
        IF DATEPART(HOUR, GETDATE()) < 8 OR DATEPART(HOUR, GETDATE()) > 18
        BEGIN
            RAISERROR('Chỉ được kết nối trong giờ làm việc (8:00-18:00)', 16, 1);
            ROLLBACK;
        END;
    END;
END;
GO

-- CẢNH BÁO: Logon trigger lỗi có thể chặn TẤT CẢ connections, kể cả sa!
-- Nếu bị lock out, phải dùng Dedicated Admin Connection (DAC):
-- sqlcmd -S server -A (hoặc Admin: prefix trong SSMS)
-- DISABLE TRIGGER trg_LimitConnections ON ALL SERVER;

Tóm Tắt: Khi Nào Dùng Trigger?

ScenarioDùng Trigger?Thay thế tốt hơn
Audit trail tự động✅ Có thểCDC (ít overhead hơn)
Enforce business rules⚠️ Thận trọngConstraints, Application logic
Cascade delete/updateFK với CASCADE
Tính toán derived fields❌ TránhComputed columns
Sync giữa tables⚠️Application logic, Service Bus
Updatable views✅ INSTEAD OFN/A
Prevent DDL changes✅ DDL TriggerDENY permissions
Chặn specific users✅ Logon TriggerDENY LOGIN

Golden Rule: Trigger là công cụ cuối cùng. Nếu có thể giải quyết bằng constraints, computed columns, application logic, hoặc CDC - hãy dùng chúng thay thế.

Views, CTEs & Window Functions

Phần này bao gồm các kỹ thuật truy vấn nâng cao trong SQL Server: Views (bao gồm Indexed Views), CTEs (Common Table Expressions), Window Functions, và Cursors.


PHẦN 1: Views

1.1 Tạo và Quản Lý Views

-- Tạo view cơ bản
CREATE VIEW dbo.vw_ActiveOrders
AS
SELECT 
    o.OrderId,
    o.OrderDate,
    o.TotalAmount,
    o.Status,
    c.CustomerName,
    c.Email,
    c.Region
FROM dbo.Orders o
INNER JOIN dbo.Customers c ON o.CustomerId = c.CustomerId
WHERE o.IsDeleted = 0;
GO

-- Sửa view (giữ nguyên permissions)
ALTER VIEW dbo.vw_ActiveOrders
AS
SELECT 
    o.OrderId,
    o.OrderDate,
    o.TotalAmount,
    o.Status,
    c.CustomerName,
    c.Email,
    c.Region,
    o.ShippedDate -- Thêm cột mới
FROM dbo.Orders o
INNER JOIN dbo.Customers c ON o.CustomerId = c.CustomerId
WHERE o.IsDeleted = 0 AND o.Status <> 'Cancelled';
GO

-- Xóa view
DROP VIEW IF EXISTS dbo.vw_ActiveOrders;

-- Xem định nghĩa view
SELECT OBJECT_DEFINITION(OBJECT_ID('dbo.vw_ActiveOrders'));
-- hoặc
EXEC sp_helptext 'dbo.vw_ActiveOrders';

1.2 WITH SCHEMABINDING

-- SCHEMABINDING: Ngăn thay đổi base objects
-- BẮT BUỘC cho Indexed Views
-- Yêu cầu: Phải dùng tên đầy đủ (schema.table)

CREATE VIEW dbo.vw_ProductPriceList
WITH SCHEMABINDING -- Khóa schema
AS
SELECT 
    p.ProductId,
    p.ProductName,
    p.Price,
    c.CategoryName
FROM dbo.Products p           -- Phải có dbo.
INNER JOIN dbo.Categories c ON p.CategoryId = c.CategoryId
WHERE p.IsActive = 1;
GO

-- Thử DROP column của base table → LỖI
-- ALTER TABLE dbo.Products DROP COLUMN Price;
-- ERROR: Cannot DROP COLUMN 'Price' because it is referenced by view 'vw_ProductPriceList'

-- Kiểm tra view có SCHEMABINDING không
SELECT 
    v.name,
    v.with_check_option,
    m.is_schema_bound
FROM sys.views v
JOIN sys.sql_modules m ON v.object_id = m.object_id
WHERE v.name = 'vw_ProductPriceList';

1.3 WITH CHECK OPTION

-- CHECK OPTION: Đảm bảo INSERT/UPDATE qua view phải thỏa WHERE của view
CREATE VIEW dbo.vw_ActiveProducts
AS
SELECT ProductId, ProductName, Price, IsActive
FROM dbo.Products
WHERE IsActive = 1
WITH CHECK OPTION; -- Ngăn INSERT/UPDATE tạo ra rows không visible trong view

-- INSERT thỏa điều kiện → OK
INSERT INTO dbo.vw_ActiveProducts (ProductName, Price, IsActive)
VALUES ('New Product', 99.99, 1);

-- INSERT vi phạm điều kiện → ERROR
INSERT INTO dbo.vw_ActiveProducts (ProductName, Price, IsActive)
VALUES ('Inactive Product', 99.99, 0);
-- ERROR: The attempted insert or update failed because the target view
-- either specifies WITH CHECK OPTION or spans a view that specifies WITH CHECK OPTION

1.4 Updatable Views

View có thể UPDATE/INSERT/DELETE nếu thỏa:

Ràng buộcMô tả
Không có DISTINCT
Không có aggregate (SUM, COUNT…)
Không có GROUP BY, HAVING
Không có TOP hoặc UNION
Chỉ reference một base table trong DML
-- View đơn giản → updatable
CREATE VIEW dbo.vw_EmployeeNames AS
SELECT EmployeeId, FirstName, LastName FROM Employees;

-- UPDATE trực tiếp qua view
UPDATE dbo.vw_EmployeeNames 
SET LastName = 'Nguyen' 
WHERE EmployeeId = 1;
-- Tương đương: UPDATE Employees SET LastName = 'Nguyen' WHERE EmployeeId = 1

-- View join nhiều bảng → KHÔNG updatable trực tiếp
-- Dùng INSTEAD OF trigger (xem phần Triggers)

1.5 Indexed Views (Materialized Views)

Indexed View lưu kết quả vật lý trên disk, tự động cập nhật khi base tables thay đổi.

-- Bước 1: Tạo view với SCHEMABINDING (bắt buộc)
CREATE VIEW dbo.vw_DailySalesSummary
WITH SCHEMABINDING
AS
SELECT 
    CAST(o.OrderDate AS DATE) AS SaleDate,
    p.CategoryId,
    COUNT_BIG(*) AS OrderCount,      -- Phải dùng COUNT_BIG(*), không phải COUNT(*)
    SUM(od.Quantity * od.UnitPrice) AS TotalRevenue,
    SUM(CAST(od.Quantity AS BIGINT)) AS TotalQuantity
FROM dbo.Orders o
INNER JOIN dbo.OrderDetails od ON o.OrderId = od.OrderId
INNER JOIN dbo.Products p ON od.ProductId = p.ProductId
WHERE o.IsDeleted = 0
GROUP BY CAST(o.OrderDate AS DATE), p.CategoryId;
GO

-- Bước 2: Tạo UNIQUE CLUSTERED INDEX → materialize view
CREATE UNIQUE CLUSTERED INDEX UCIX_vw_DailySalesSummary
ON dbo.vw_DailySalesSummary (SaleDate, CategoryId);
GO

-- Bước 3 (Optional): Thêm non-clustered indexes
CREATE INDEX IX_vw_DailySalesSummary_Revenue
ON dbo.vw_DailySalesSummary (TotalRevenue DESC);

-- Optimizer tự động dùng indexed view khi profitable
-- (với ENTERPRISE edition, STANDARD cần NOEXPAND hint)

-- Force dùng indexed view (STANDARD edition)
SELECT SaleDate, CategoryId, TotalRevenue
FROM dbo.vw_DailySalesSummary WITH (NOEXPAND)
WHERE SaleDate = '2025-01-01';

Yêu cầu cho Indexed View:

Yêu cầuChi tiết
WITH SCHEMABINDINGBắt buộc
First index là UNIQUE CLUSTEREDBắt buộc
Không có *Phải explicit columns
Không DISTINCT, TOP, subqueries
Không OUTER JOINChỉ INNER JOIN
COUNT_BIG(*) thay vì COUNT(*)Khi có GROUP BY
Deterministic functions only
SET ANSI_NULLS ONSET QUOTED_IDENTIFIER ON

1.6 sys.views và INFORMATION_SCHEMA.VIEWS

-- Liệt kê tất cả views
SELECT 
    v.name AS ViewName,
    s.name AS SchemaName,
    v.create_date,
    v.modify_date,
    m.is_schema_bound,
    m.uses_ansi_nulls
FROM sys.views v
JOIN sys.schemas s ON v.schema_id = s.schema_id
JOIN sys.sql_modules m ON v.object_id = m.object_id
ORDER BY s.name, v.name;

-- INFORMATION_SCHEMA.VIEWS: Portable across SQL products
SELECT 
    TABLE_SCHEMA,
    TABLE_NAME,
    VIEW_DEFINITION
FROM INFORMATION_SCHEMA.VIEWS
WHERE TABLE_NAME LIKE 'vw_%';

-- Tìm views phụ thuộc vào một bảng
SELECT DISTINCT 
    v.name AS ViewName
FROM sys.views v
JOIN sys.sql_expression_dependencies d ON v.object_id = d.referencing_id
JOIN sys.objects o ON d.referenced_id = o.object_id
WHERE o.name = 'Orders';

PHẦN 2: CTEs (Common Table Expressions)

2.1 Cú Pháp Cơ Bản

-- CTE cơ bản
WITH OrderSummary AS (
    SELECT 
        CustomerId,
        COUNT(*) AS TotalOrders,
        SUM(TotalAmount) AS TotalSpent
    FROM Orders
    WHERE Status = 'Completed'
    GROUP BY CustomerId
)
SELECT 
    c.CustomerName,
    c.Email,
    os.TotalOrders,
    os.TotalSpent
FROM Customers c
INNER JOIN OrderSummary os ON c.CustomerId = os.CustomerId
WHERE os.TotalSpent > 1000
ORDER BY os.TotalSpent DESC;

2.2 Multiple CTEs trong Một Query

WITH
-- CTE 1: High-value customers
HighValueCustomers AS (
    SELECT CustomerId, SUM(TotalAmount) AS TotalSpent
    FROM Orders
    GROUP BY CustomerId
    HAVING SUM(TotalAmount) >= 5000
),
-- CTE 2: Recent orders (dùng bảng gốc)
RecentOrders AS (
    SELECT CustomerId, COUNT(*) AS OrdersLast30Days
    FROM Orders
    WHERE OrderDate >= DATEADD(DAY, -30, GETDATE())
    GROUP BY CustomerId
),
-- CTE 3: Combine (có thể reference các CTEs trước)
CustomerInsights AS (
    SELECT 
        hvc.CustomerId,
        hvc.TotalSpent,
        ISNULL(ro.OrdersLast30Days, 0) AS RecentOrders
    FROM HighValueCustomers hvc
    LEFT JOIN RecentOrders ro ON hvc.CustomerId = ro.CustomerId
)
-- Final query sử dụng CTE cuối cùng
SELECT 
    c.CustomerName,
    ci.TotalSpent,
    ci.RecentOrders,
    CASE 
        WHEN ci.RecentOrders >= 3 THEN 'Very Active'
        WHEN ci.RecentOrders >= 1 THEN 'Active'
        ELSE 'Inactive'
    END AS ActivityStatus
FROM Customers c
INNER JOIN CustomerInsights ci ON c.CustomerId = ci.CustomerId
ORDER BY ci.TotalSpent DESC;

2.3 Recursive CTEs

-- CẤU TRÚC:
-- Anchor Member UNION ALL Recursive Member

-- EXAMPLE 1: Traversal hierarchy nhân viên
WITH EmployeeTree AS (
    -- ANCHOR: CEO (không có manager)
    SELECT 
        EmployeeId,
        FullName,
        ManagerId,
        JobTitle,
        0 AS Level,
        CAST(FullName AS NVARCHAR(MAX)) AS HierarchyPath,
        CAST(EmployeeId AS NVARCHAR(MAX)) AS IdPath
    FROM Employees
    WHERE ManagerId IS NULL

    UNION ALL

    -- RECURSIVE: Direct reports của các employees đã có
    SELECT 
        e.EmployeeId,
        e.FullName,
        e.ManagerId,
        e.JobTitle,
        et.Level + 1 AS Level,
        et.HierarchyPath + ' > ' + e.FullName,
        et.IdPath + '.' + CAST(e.EmployeeId AS NVARCHAR(10))
    FROM Employees e
    INNER JOIN EmployeeTree et ON e.ManagerId = et.EmployeeId
)
SELECT 
    REPLICATE('  ', Level) + FullName AS OrgChart,
    JobTitle,
    Level,
    HierarchyPath
FROM EmployeeTree
ORDER BY IdPath;
-- OPTION (MAXRECURSION 100); -- Giới hạn depth (default là 100, 0 = unlimited)
-- EXAMPLE 2: Đếm ngày làm việc (trừ cuối tuần)
WITH DateSeries AS (
    -- Anchor: ngày bắt đầu
    SELECT CAST('2025-01-01' AS DATE) AS WorkDate

    UNION ALL

    -- Recursive: thêm từng ngày
    SELECT DATEADD(DAY, 1, WorkDate)
    FROM DateSeries
    WHERE WorkDate < '2025-12-31'
)
SELECT COUNT(*) AS WorkingDays
FROM DateSeries
WHERE DATENAME(WEEKDAY, WorkDate) NOT IN ('Saturday', 'Sunday')
OPTION (MAXRECURSION 366);
-- EXAMPLE 3: Tìm routes giữa các nodes (graph traversal)
WITH RouteSearch AS (
    SELECT 
        FromCity, ToCity, 
        Distance AS TotalDistance,
        CAST(FromCity + ' → ' + ToCity AS NVARCHAR(MAX)) AS Route,
        1 AS Hops
    FROM Routes
    WHERE FromCity = 'Hanoi'

    UNION ALL

    SELECT 
        rs.FromCity, r.ToCity,
        rs.TotalDistance + r.Distance,
        rs.Route + ' → ' + r.ToCity,
        rs.Hops + 1
    FROM RouteSearch rs
    INNER JOIN Routes r ON rs.ToCity = r.FromCity
    WHERE rs.Hops < 5 -- Giới hạn số bước
    AND rs.Route NOT LIKE '%' + r.ToCity + '%' -- Tránh cycle
)
SELECT Route, TotalDistance
FROM RouteSearch
WHERE ToCity = 'HCMC'
ORDER BY TotalDistance;

2.4 CTE cho DELETE và UPDATE

-- DELETE với CTE (xóa duplicates)
WITH DuplicateOrders AS (
    SELECT 
        OrderId,
        ROW_NUMBER() OVER (
            PARTITION BY CustomerId, OrderDate, TotalAmount
            ORDER BY OrderId
        ) AS RowNum
    FROM Orders
)
DELETE FROM DuplicateOrders WHERE RowNum > 1;
-- Thực tế: DELETE ảnh hưởng đến bảng Orders gốc

-- UPDATE với CTE (cập nhật với ranking)
WITH RankedSalary AS (
    SELECT 
        EmployeeId,
        Salary,
        DENSE_RANK() OVER (PARTITION BY DepartmentId ORDER BY Salary DESC) AS SalaryRank
    FROM Employees
)
UPDATE RankedSalary
SET Salary = Salary * 1.10 -- Tăng 10% cho top earner
WHERE SalaryRank = 1;

2.5 CTE vs Subquery vs Temp Table

CTESubqueryTemp Table
Readable✅ Rất rõ❌ Lồng nhau✅ Rõ
Reuse❌ Chỉ một lần✅ Nhiều lần
Recursive
Statistics
Large data⚠️⚠️
Index
DELETE/UPDATE

PHẦN 3: Window Functions

3.1 Cú Pháp OVER()

function_name(args) OVER (
    [PARTITION BY partition_expression]
    [ORDER BY sort_expression [ASC|DESC]]
    [ROWS|RANGE BETWEEN frame_start AND frame_end]
)

-- Frame options:
-- UNBOUNDED PRECEDING: Từ đầu partition
-- CURRENT ROW: Row hiện tại
-- UNBOUNDED FOLLOWING: Đến cuối partition
-- N PRECEDING: N rows trước
-- N FOLLOWING: N rows sau

3.2 Ranking Functions

SELECT 
    EmployeeId,
    FullName,
    Department,
    Salary,
    
    -- ROW_NUMBER: Sequential, không ties
    ROW_NUMBER() OVER (PARTITION BY Department ORDER BY Salary DESC) AS RowNum,
    
    -- RANK: Ties có cùng rank, skip sau
    RANK() OVER (PARTITION BY Department ORDER BY Salary DESC) AS Rnk,
    
    -- DENSE_RANK: Ties có cùng rank, không skip
    DENSE_RANK() OVER (PARTITION BY Department ORDER BY Salary DESC) AS DenseRnk,
    
    -- NTILE: Chia thành N buckets đều nhau
    NTILE(4) OVER (PARTITION BY Department ORDER BY Salary DESC) AS Quartile
FROM Employees
ORDER BY Department, Salary DESC;

/*
Kết quả ví dụ (cùng Department, Salary: 10000, 8000, 8000, 6000):
FullName  Salary  RowNum  Rnk  DenseRnk  Quartile
Alice     10000   1       1    1         1
Bob       8000    2       2    2         2
Carol     8000    3       2    2         2
David     6000    4       4    3         3
*/

3.3 Offset Functions

SELECT
    OrderDate,
    Revenue,
    
    -- LAG: Giá trị từ row TRƯỚC đó
    LAG(Revenue, 1, 0) OVER (ORDER BY OrderDate) AS PrevRevenue,
    LAG(Revenue, 2, 0) OVER (ORDER BY OrderDate) AS TwoPeriodsAgo,
    
    -- LEAD: Giá trị từ row SAU đó
    LEAD(Revenue, 1, 0) OVER (ORDER BY OrderDate) AS NextRevenue,
    
    -- FIRST_VALUE: Giá trị đầu tiên trong partition/window
    FIRST_VALUE(Revenue) OVER (PARTITION BY YEAR(OrderDate) ORDER BY OrderDate
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS FirstRevenueOfYear,
    
    -- LAST_VALUE: Giá trị cuối cùng (cần explicit frame!)
    LAST_VALUE(Revenue) OVER (PARTITION BY YEAR(OrderDate) ORDER BY OrderDate
        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS LastRevenueOfYear,
    
    -- Month-over-Month Growth %
    CASE 
        WHEN LAG(Revenue) OVER (ORDER BY OrderDate) = 0 THEN NULL
        ELSE ROUND(
            (Revenue - LAG(Revenue) OVER (ORDER BY OrderDate)) * 100.0 
            / LAG(Revenue) OVER (ORDER BY OrderDate), 
            2)
    END AS MoMGrowthPct

FROM MonthlySales
ORDER BY OrderDate;

3.4 Aggregate Window Functions

-- RUNNING TOTAL (Cumulative Sum)
SELECT
    OrderDate,
    OrderAmount,
    
    -- Running total toàn bộ
    SUM(OrderAmount) OVER (
        ORDER BY OrderDate
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS RunningTotal,
    
    -- Running total theo năm (PARTITION BY)
    SUM(OrderAmount) OVER (
        PARTITION BY YEAR(OrderDate)
        ORDER BY OrderDate
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS YearlyRunningTotal,
    
    -- Moving Average: trung bình 7 ngày gần nhất
    AVG(OrderAmount) OVER (
        ORDER BY OrderDate
        ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
    ) AS MovingAvg7Day,
    
    -- Running Count
    COUNT(*) OVER (
        PARTITION BY CustomerId
        ORDER BY OrderDate
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS CustomerOrderNumber,

    -- Percent of total
    ROUND(
        OrderAmount * 100.0 / SUM(OrderAmount) OVER (),
        2
    ) AS PctOfTotal,
    
    -- Percent of year total
    ROUND(
        OrderAmount * 100.0 / SUM(OrderAmount) OVER (PARTITION BY YEAR(OrderDate)),
        2
    ) AS PctOfYearTotal

FROM Orders
ORDER BY OrderDate;

3.5 Ví Dụ Thực Tế

Pagination với ROW_NUMBER()

DECLARE @Page INT = 2;
DECLARE @PageSize INT = 10;

WITH PagedOrders AS (
    SELECT 
        OrderId,
        OrderDate,
        TotalAmount,
        CustomerName,
        ROW_NUMBER() OVER (ORDER BY OrderDate DESC, OrderId DESC) AS RowNum
    FROM Orders o
    JOIN Customers c ON o.CustomerId = c.CustomerId
    WHERE o.IsDeleted = 0
)
SELECT OrderId, OrderDate, TotalAmount, CustomerName
FROM PagedOrders
WHERE RowNum BETWEEN (@Page - 1) * @PageSize + 1 AND @Page * @PageSize;

-- Modern approach: OFFSET/FETCH (SQL 2012+)
SELECT OrderId, OrderDate, TotalAmount
FROM Orders
ORDER BY OrderDate DESC, OrderId DESC
OFFSET (@Page - 1) * @PageSize ROWS
FETCH NEXT @PageSize ROWS ONLY;

Tìm và Xóa Duplicates

-- Xem duplicates
WITH DuplicateDetection AS (
    SELECT 
        *,
        ROW_NUMBER() OVER (
            PARTITION BY Email, FirstName, LastName -- Cột xác định duplicate
            ORDER BY CustomerId ASC -- Keep: record có ID nhỏ nhất
        ) AS RowNum,
        COUNT(*) OVER (PARTITION BY Email, FirstName, LastName) AS DupeCount
    FROM Customers
)
SELECT * FROM DuplicateDetection WHERE DupeCount > 1;

-- Xóa duplicates (giữ record đầu tiên)
WITH DuplicateDetection AS (
    SELECT 
        CustomerId,
        ROW_NUMBER() OVER (
            PARTITION BY Email, FirstName, LastName
            ORDER BY CustomerId ASC
        ) AS RowNum
    FROM Customers
)
DELETE FROM DuplicateDetection WHERE RowNum > 1;

Running Total với ROWS vs RANGE

-- ROWS: Tính theo vị trí vật lý (accurate)
SELECT 
    SaleDate, Amount,
    SUM(Amount) OVER (ORDER BY SaleDate ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS RunningTotal_ROWS
FROM Sales;

-- RANGE: Tính theo giá trị, bao gồm cả ties (có thể không chính xác với duplicates)
SELECT 
    SaleDate, Amount,
    SUM(Amount) OVER (ORDER BY SaleDate RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS RunningTotal_RANGE
FROM Sales;

-- Khi có 2 rows cùng SaleDate:
-- ROWS: Row 1 là subtotal đến row 1, Row 2 là subtotal đến row 2
-- RANGE: Cả hai rows đều hiển thị cùng tổng (tổng đến cuối ngày đó)
-- ROWS nhanh hơn và thường là điều bạn muốn

Gaps and Islands

-- Tìm các dải ngày liên tiếp user login (Islands)
WITH LoginDates AS (
    SELECT DISTINCT UserId, CAST(LoginDateTime AS DATE) AS LoginDate
    FROM UserLogins
),
Ranked AS (
    SELECT 
        UserId, LoginDate,
        ROW_NUMBER() OVER (PARTITION BY UserId ORDER BY LoginDate) AS RowNum
    FROM LoginDates
),
Islands AS (
    SELECT
        UserId,
        LoginDate,
        DATEADD(DAY, -RowNum, LoginDate) AS IslandKey -- Ngày - RowNum = hằng số khi liên tiếp
    FROM Ranked
)
SELECT
    UserId,
    MIN(LoginDate) AS StreakStart,
    MAX(LoginDate) AS StreakEnd,
    COUNT(*) AS ConsecutiveDays,
    DATEDIFF(DAY, MIN(LoginDate), MAX(LoginDate)) + 1 AS TotalDays
FROM Islands
GROUP BY UserId, IslandKey
HAVING COUNT(*) >= 3 -- Chỉ những streak >= 3 ngày
ORDER BY UserId, StreakStart;

-- Tìm Gaps (khoảng trống)
WITH AllDates AS (
    SELECT 
        UserId, LoginDate,
        LEAD(LoginDate) OVER (PARTITION BY UserId ORDER BY LoginDate) AS NextLogin
    FROM (
        SELECT DISTINCT UserId, CAST(LoginDateTime AS DATE) AS LoginDate
        FROM UserLogins
    ) d
)
SELECT 
    UserId, 
    LoginDate AS GapStart,
    NextLogin AS GapEnd,
    DATEDIFF(DAY, LoginDate, NextLogin) - 1 AS GapDays
FROM AllDates
WHERE DATEDIFF(DAY, LoginDate, NextLogin) > 1; -- Gap lớn hơn 1 ngày

PHẦN 4: Cursors

4.1 Cú Pháp Cursor

-- Cú pháp đầy đủ
DECLARE cursor_name CURSOR
    [LOCAL | GLOBAL]
    [FORWARD_ONLY | SCROLL]
    [STATIC | KEYSET | DYNAMIC | FAST_FORWARD]
    [READ_ONLY | SCROLL_LOCKS | OPTIMISTIC]
FOR
    select_statement
[FOR UPDATE [OF column_name [,...n]]];

OPEN cursor_name;

FETCH [NEXT | PRIOR | FIRST | LAST | ABSOLUTE n | RELATIVE n] 
FROM cursor_name INTO @var1 [, @var2 ...];

WHILE @@FETCH_STATUS = 0
BEGIN
    -- Process...
    FETCH NEXT FROM cursor_name INTO @var1;
END;

CLOSE cursor_name;
DEALLOCATE cursor_name;

4.2 Cursor Types

TypeMô tảUse Case
FAST_FORWARDRead-only, forward-only, nhanh nhấtKhi chỉ cần đi qua một lần
STATICSnapshot data vào tempdb, data cố địnhKhi data có thể thay đổi trong khi đọc
KEYSETKeys được lưu, data rows được đọc lạiBiến đổi data visible, insert không
DYNAMICPhản ánh mọi thay đổi real-timeKhi cần thấy live changes
-- FAST_FORWARD: Nhanh nhất, chỉ read-only, forward
DECLARE @OrderId INT;
DECLARE @Amount DECIMAL(18,2);

DECLARE cur_Orders CURSOR FAST_FORWARD FOR
    SELECT OrderId, TotalAmount
    FROM Orders
    WHERE Status = 'Pending'
    ORDER BY OrderId;

OPEN cur_Orders;
FETCH NEXT FROM cur_Orders INTO @OrderId, @Amount;

WHILE @@FETCH_STATUS = 0
BEGIN
    -- Gọi SP cho từng order (lý do duy nhất dùng cursor)
    EXEC dbo.ProcessPendingOrder @OrderId, @Amount;
    
    FETCH NEXT FROM cur_Orders INTO @OrderId, @Amount;
END;

CLOSE cur_Orders;
DEALLOCATE cur_Orders;

4.3 Tại Sao Tránh Cursors

-- BENCHMARK: Cursor vs Set-based

-- Cursor approach: O(n) round trips
DECLARE @Id INT;
DECLARE cur CURSOR FOR SELECT ProductId FROM Products;
OPEN cur;
FETCH NEXT FROM cur INTO @Id;
WHILE @@FETCH_STATUS = 0
BEGIN
    UPDATE Products SET Stock = Stock - 1 WHERE ProductId = @Id; -- 1 update mỗi lần
    FETCH NEXT FROM cur INTO @Id;
END;
CLOSE cur; DEALLOCATE cur;
-- Với 100,000 products: 100,000 update statements, 100,000 lock acquisitions

-- Set-based approach: O(1) - một câu lệnh
UPDATE Products SET Stock = Stock - 1; -- 1 update cho tất cả
-- Với 100,000 products: 1 operation, optimized I/O, parallel execution

4.4 Alternatives to Cursors

-- THAY CURSOR BẰNG:

-- 1. Set-based operations
-- Cursor: update từng row
-- Better: UPDATE Table SET col = expr WHERE condition

-- 2. Window Functions
-- Cursor: dùng biến để track previous row
-- Better: LAG(), ROW_NUMBER(), SUM() OVER()

-- 3. Recursive CTE
-- Cursor: duyệt hierarchy từng node
-- Better: Recursive CTE

-- 4. APPLY (cho SP/function per row)
-- Cursor: EXEC SP cho từng row
-- Nếu SP có thể convert thành TVF, dùng CROSS APPLY

-- 5. Batch processing (nếu phải xử lý từng batch)
DECLARE @BatchSize INT = 1000;
DECLARE @LastId INT = 0;

WHILE 1 = 1
BEGIN
    UPDATE TOP (@BatchSize) p
    SET p.ProcessedFlag = 1
    OUTPUT inserted.ProductId
    FROM Products p
    WHERE p.ProcessedFlag = 0 AND p.ProductId > @LastId
    ORDER BY p.ProductId;

    IF @@ROWCOUNT = 0 BREAK;
    
    SET @LastId = @LastId + @BatchSize;
    WAITFOR DELAY '00:00:01'; -- Giảm tải
END;

-- 6. STRING_AGG thay vì cursor concatenate strings
-- Cursor approach: dùng biến + concatenation
-- Better:
SELECT 
    DepartmentId,
    STRING_AGG(FullName, ', ') WITHIN GROUP (ORDER BY FullName) AS EmployeeList
FROM Employees
GROUP BY DepartmentId;

Tóm Tắt So Sánh

Khi nào dùng gì?

Kỹ thuậtDùng khiTránh khi
ViewSimplify complex queries, security layerCần parameters (dùng TVF thay)
Indexed ViewPre-aggregate large fact tablesBảng hay INSERT/UPDATE (overhead cao)
CTEReadable queries, recursive, DELETE/UPDATEDataset lớn cần index (dùng temp table)
Window FunctionRanking, running totals, lag/leadKhông có ORDER BY trong OVER() (cho một số functions)
CursorGọi SP từng row, không thể set-basedMọi trường hợp khác

Performance Quick Reference

-- Checking view dependencies
SELECT 
    referencing_entity_name,
    referenced_entity_name
FROM sys.dm_sql_referencing_entities('dbo.Orders', 'OBJECT');

-- Find expensive window function queries
SELECT TOP 10
    qs.total_elapsed_time / qs.execution_count AS avg_elapsed_time,
    qs.execution_count,
    SUBSTRING(qt.text, 1, 200) AS query_snippet
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
WHERE qt.text LIKE '%OVER%'
ORDER BY avg_elapsed_time DESC;

Transactions & Concurrency - Tổng quan

Phần này bao gồm các kiến thức về Giao dịch (Transactions) và Kiểm soát đồng thời (Concurrency Control) trong SQL Server — hai chủ đề cốt lõi trong phát triển ứng dụng enterprise và phỏng vấn kỹ thuật.

Các chủ đề con

Chủ đềMô tả
Giao dịch (Transactions)ACID, BEGIN/COMMIT/ROLLBACK, Savepoints, WAL, XACT_ABORT
Isolation LevelsDirty Read, Phantom Read, RCSI, Snapshot Isolation, NOLOCK
Locking, Blocking & DeadlocksLock types, Lock escalation, Deadlock detection & prevention

Q&A Phỏng vấn


🟢 Junior Level

Q1: ACID là gì? Giải thích từng thuộc tính.

A:

  • Atomicity (Tính nguyên tử): Tất cả các thao tác trong transaction phải thành công toàn bộ hoặc không có thao tác nào được thực hiện. Nếu một bước thất bại, toàn bộ transaction sẽ bị ROLLBACK.
  • Consistency (Tính nhất quán): Transaction phải đưa database từ trạng thái hợp lệ này sang trạng thái hợp lệ khác. Các ràng buộc (constraints), triggers, cascade phải được đảm bảo.
  • Isolation (Tính cô lập): Các transaction đang chạy đồng thời không được nhìn thấy dữ liệu lẫn nhau ở trạng thái trung gian. Mức độ cô lập được điều chỉnh qua Isolation Levels.
  • Durability (Tính bền vững): Sau khi transaction COMMIT, dữ liệu phải được lưu trữ vĩnh viễn kể cả khi hệ thống bị sập. SQL Server đảm bảo điều này qua Write-Ahead Logging (WAL).

Q2: Cú pháp cơ bản của Transaction trong SQL Server là gì?

A:

BEGIN TRANSACTION;
    UPDATE Accounts SET Balance = Balance - 100 WHERE AccountId = 1;
    UPDATE Accounts SET Balance = Balance + 100 WHERE AccountId = 2;
COMMIT TRANSACTION;

-- Hoặc khi có lỗi:
BEGIN TRANSACTION;
    UPDATE Accounts SET Balance = Balance - 100 WHERE AccountId = 1;
    -- Phát hiện lỗi...
ROLLBACK TRANSACTION;

Q3: Sự khác biệt giữa COMMIT và ROLLBACK là gì?

A:

  • COMMIT: Xác nhận tất cả thay đổi trong transaction, ghi vĩnh viễn vào database.
  • ROLLBACK: Hủy bỏ tất cả thay đổi trong transaction, đưa dữ liệu về trạng thái trước khi bắt đầu transaction.

Q4: Isolation Level mặc định của SQL Server là gì?

A: READ COMMITTED — ngăn chặn Dirty Read nhưng vẫn cho phép Non-repeatable Read và Phantom Read. Đây là mức cân bằng tốt giữa tính nhất quán và hiệu suất.


Q5: Dirty Read là gì? Cho ví dụ.

A: Dirty Read xảy ra khi một transaction đọc dữ liệu mà transaction khác đang sửa đổi nhưng chưa COMMIT. Nếu transaction kia sau đó ROLLBACK, dữ liệu đã đọc là không hợp lệ.

-- Session 1: Cập nhật nhưng chưa commit
BEGIN TRANSACTION;
UPDATE Products SET Price = 999 WHERE ProductId = 1;
-- Chưa COMMIT

-- Session 2 (READ UNCOMMITTED): Đọc được giá 999 - là Dirty Read
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT Price FROM Products WHERE ProductId = 1; -- Trả về 999

-- Session 1: Rollback
ROLLBACK; -- Giá thực tế vẫn là giá cũ

Q6: Blocking trong SQL Server là gì?

A: Blocking xảy ra khi một transaction đang giữ lock trên resource, khiến transaction khác phải chờ. Ví dụ: Session A đang UPDATE mà chưa COMMIT, Session B muốn SELECT cùng row đó sẽ bị block.


Q7: Làm thế nào để xem các blocking session hiện tại?

A:

-- Cách nhanh nhất
EXEC sp_who2;

-- Chi tiết hơn
SELECT 
    r.session_id,
    r.blocking_session_id,
    r.wait_type,
    r.wait_time,
    r.status,
    t.text AS QueryText
FROM sys.dm_exec_requests r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
WHERE r.blocking_session_id > 0;

Q8: Deadlock là gì?

A: Deadlock xảy ra khi hai hoặc nhiều transaction đều đang chờ nhau giải phóng lock theo dạng vòng tròn (circular dependency). SQL Server tự động phát hiện deadlock, chọn một transaction làm deadlock victim và ROLLBACK nó, giải phóng deadlock.


Q9: NOLOCK hint là gì và có rủi ro gì không?

A: WITH (NOLOCK) tương đương với READ UNCOMMITTED — cho phép đọc dữ liệu chưa commit. Rủi ro nghiêm trọng:

  • Đọc được dữ liệu sẽ bị ROLLBACK (Dirty Read)
  • Có thể bỏ sót rows (skip rows)
  • Có thể đọc trùng rows (duplicate rows)
  • Không nên dùng trong hệ thống tài chính, báo cáo cần chính xác.

Q10: @@TRANCOUNT là gì?

A: @@TRANCOUNT trả về số lượng transaction đang mở trong session hiện tại. Mỗi BEGIN TRANSACTION tăng giá trị lên 1, mỗi COMMIT giảm 1, chỉ ROLLBACK mới đặt về 0.


🟡 Mid Level

Q11: Phantom Read là gì? Isolation level nào ngăn chặn nó?

A: Phantom Read xảy ra khi một transaction thực hiện cùng một query hai lần nhưng nhận được kết quả khác nhau vì transaction khác đã INSERT/DELETE rows phù hợp với điều kiện query giữa hai lần đọc.

Chỉ SERIALIZABLE isolation level mới ngăn chặn Phantom Read hoàn toàn (dùng key-range locks). SNAPSHOT ISOLATION cũng ngăn phantom read nhờ row versioning.


Q12: READ COMMITTED SNAPSHOT ISOLATION (RCSI) khác READ COMMITTED như thế nào?

A:

Đặc điểmREAD COMMITTEDRCSI
Cơ chếShared locksRow versioning (tempdb)
Readers block WritersKhông
Writers block ReadersKhông
Dirty ReadKhôngKhông
OverheadLock overheadVersion store trong tempdb
Mặc định Azure SQLKhông

RCSI cho writers và readers không block nhau — tốt hơn cho concurrency cao. Bật bằng: ALTER DATABASE MyDB SET READ_COMMITTED_SNAPSHOT ON.


Q13: Giải thích Lock Escalation và khi nào nó xảy ra.

A: SQL Server bắt đầu với row-level locks. Khi số locks trong một statement vượt ngưỡng (~5000 locks), SQL Server escalate (nâng cấp) thành table lock để tiết kiệm bộ nhớ. Điều này giảm overhead quản lý lock nhưng giảm concurrency vì table bị lock toàn bộ.

Để hạn chế lock escalation:

-- Tắt lock escalation cho table cụ thể
ALTER TABLE dbo.BigTable SET (LOCK_ESCALATION = DISABLE);

-- Dùng ROWLOCK hint trong query
SELECT * FROM BigTable WITH (ROWLOCK) WHERE Id = 1;

Q14: TRY-CATCH với Transaction hoạt động như thế nào?

A:

BEGIN TRANSACTION;
BEGIN TRY
    UPDATE Accounts SET Balance = Balance - 500 WHERE AccountId = 1;
    UPDATE Accounts SET Balance = Balance + 500 WHERE AccountId = 2;
    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    IF @@TRANCOUNT > 0
        ROLLBACK TRANSACTION;
    -- Re-throw error
    THROW;
END CATCH;

Lưu ý: Luôn kiểm tra @@TRANCOUNT > 0 trước khi ROLLBACK để tránh lỗi khi transaction đã bị rollback do XACT_ABORT.


Q15: XACT_ABORT là gì và khi nào nên dùng?

A: SET XACT_ABORT ON khiến SQL Server tự động ROLLBACK toàn bộ transaction khi có bất kỳ lỗi runtime nào xảy ra (thay vì chỉ fail statement đó). Đây là best practice trong stored procedures.

SET XACT_ABORT ON;
BEGIN TRANSACTION;
    -- Nếu bất kỳ statement nào fail, toàn bộ transaction sẽ rollback tự động
    INSERT INTO Orders ...;
    INSERT INTO OrderDetails ...;
COMMIT TRANSACTION;

Q16: Savepoints là gì và dùng khi nào?

A: Savepoints cho phép ROLLBACK một phần của transaction về đến điểm lưu, không phải toàn bộ transaction.

BEGIN TRANSACTION;
    INSERT INTO Log VALUES ('Step 1');
    SAVE TRANSACTION step1; -- Đặt savepoint

    INSERT INTO Orders VALUES (...);
    -- Nếu có lỗi, rollback về step1, không phải về đầu transaction
    ROLLBACK TRANSACTION step1;

    INSERT INTO Log VALUES ('Step 1 still committed');
COMMIT TRANSACTION;

Q17: Làm thế nào để phát hiện và xử lý Deadlock?

A:

Phát hiện:

-- Bật trace flag để log deadlock vào SQL Server Error Log
DBCC TRACEON(1222, -1); -- Detailed deadlock info
DBCC TRACEON(1204, -1); -- Basic deadlock info

-- Hoặc dùng Extended Events (recommended)
-- Event: xml_deadlock_report

Xử lý:

-- Retry pattern trong application
-- Trong SQL: Bắt error 1205 (Deadlock victim)
BEGIN TRY
    -- Transaction code
END TRY
BEGIN CATCH
    IF ERROR_NUMBER() = 1205 -- Deadlock victim
    BEGIN
        PRINT 'Deadlock detected, retry...';
        -- Application logic để retry
    END
    ELSE THROW;
END CATCH;

Q18: Sự khác biệt giữa Shared Lock (S) và Exclusive Lock (X)?

A:

  • Shared Lock (S): Dùng cho READ. Nhiều shared locks có thể tồn tại cùng lúc trên cùng resource. Không cho phép Exclusive lock.
  • Exclusive Lock (X): Dùng cho WRITE (INSERT, UPDATE, DELETE). Chỉ một exclusive lock được phép tại một thời điểm. Không compatible với bất kỳ lock nào khác.

Q19: Update Lock (U Lock) là gì? Tại sao cần thiết?

A: Update Lock (U) là bước trung gian khi SQL Server chuẩn bị UPDATE:

  1. Đầu tiên acquire U lock để đọc row (scan)
  2. Sau đó convert sang X lock khi thực sự ghi

Lý do cần thiết: Ngăn chặn deadlock trong trường hợp hai session cùng đọc rồi cùng update. Nếu dùng S lock rồi convert X lock, hai session có thể deadlock. U lock chỉ tương thích với S lock (không phải U), nên chỉ một session có thể giữ U lock tại một thời điểm.


Q20: Non-repeatable Read là gì? Khác Phantom Read như thế nào?

A:

  • Non-repeatable Read: Cùng một row được đọc hai lần trong transaction cho kết quả khác nhau (row bị UPDATE/DELETE bởi transaction khác giữa hai lần đọc).
  • Phantom Read: Cùng một query (với WHERE condition) trả về số lượng rows khác nhau (rows mới bị INSERT bởi transaction khác).
Sự cốNguyên nhânIsolation ngăn chặn
Non-repeatable ReadRow bị UPDATE/DELETEREPEATABLE READ trở lên
Phantom ReadRow mới bị INSERTSERIALIZABLE hoặc SNAPSHOT

🔴 Senior Level

Q21: Giải thích Write-Ahead Logging (WAL) và vai trò của nó trong Durability.

A: WAL là nguyên tắc cốt lõi đảm bảo Durability:

  1. Trước khi ghi data page vào disk, SQL Server phải ghi log record tương ứng vào Transaction Log trước.
  2. Khi COMMIT, SQL Server chỉ cần đảm bảo log records được flush to disk (không cần data pages).
  3. Nếu hệ thống crash, SQL Server dùng transaction log để REDO (áp lại các committed transactions) và UNDO (rollback các uncommitted transactions) trong quá trình recovery.

Checkpoint process: SQL Server định kỳ thực hiện checkpoint để flush dirty data pages từ buffer pool xuống disk, giảm thời gian recovery.


Q22: Tại sao Long-running Transactions lại nguy hiểm?

A:

  1. Transaction Log không thể reuse: Log không thể truncate nếu transaction vẫn đang mở, gây log file phình to.
  2. Blocking: Giữ locks lâu dài, blocking các session khác.
  3. Version Store bloat (nếu dùng RCSI/SI): Row versions phải giữ trong tempdb cho đến khi transaction kết thúc.
  4. Rollback time: Transaction chạy 2 giờ có thể mất 2 giờ để rollback.

Giải pháp: Chia nhỏ batch, dùng WAITFOR DELAY ‘00:00:00’ để nhường CPU, monitor với sys.dm_exec_requests.


Q23: Distributed Transaction và MSDTC hoạt động như thế nào?

A: Distributed Transaction span qua nhiều resource managers (nhiều SQL Server instances, hoặc SQL Server + Message Queue). Microsoft Distributed Transaction Coordinator (MSDTC) điều phối 2-Phase Commit:

  1. Phase 1 - Prepare: MSDTC yêu cầu tất cả participants chuẩn bị commit và báo cáo trạng thái.
  2. Phase 2 - Commit/Rollback: Nếu tất cả sẵn sàng, MSDTC ra lệnh commit. Nếu bất kỳ participant nào fail, toàn bộ rollback.
-- Linked server distributed transaction
BEGIN DISTRIBUTED TRANSACTION;
    UPDATE LocalDB.dbo.Table1 SET Col = 1;
    UPDATE [RemoteServer].RemoteDB.dbo.Table2 SET Col = 2;
COMMIT;

Nhược điểm: Latency cao, MSDTC là single point of failure. Modern architecture thường dùng Saga pattern thay thế.


Q24: Giải thích SNAPSHOT ISOLATION và sự khác biệt với RCSI.

A:

RCSISNAPSHOT ISOLATION
Kích hoạtALTER DATABASE ... SET READ_COMMITTED_SNAPSHOT ONALTER DATABASE ... SET ALLOW_SNAPSHOT_ISOLATION ON
Áp dụng choTất cả READ COMMITTED queries (tự động)Chỉ sessions SET TRANSACTION ISOLATION LEVEL SNAPSHOT
Snapshot timeStatement-level (mỗi statement thấy snapshot khác nhau)Transaction-level (toàn bộ transaction thấy cùng snapshot)
Write conflictsKhông kiểm traKiểm tra — sẽ fail nếu data đã thay đổi

SNAPSHOT ISOLATION hay phát hiện update conflicts: Nếu row đã bị modify bởi transaction khác kể từ khi snapshot được tạo, transaction hiện tại sẽ fail với lỗi.


Q25: Làm thế nào để tune performance khi có nhiều Blocking?

A:

  1. Tìm blocking chain:
SELECT 
    blocking.session_id AS blocker_session,
    blocked.session_id AS blocked_session,
    blocked.wait_time / 1000 AS wait_seconds,
    blocked.wait_type,
    SUBSTRING(sqltext.text, (blocked.statement_start_offset/2)+1, 
        ((CASE blocked.statement_end_offset WHEN -1 THEN DATALENGTH(sqltext.text)
          ELSE blocked.statement_end_offset END - blocked.statement_start_offset)/2)+1) AS blocked_query
FROM sys.dm_exec_requests blocked
INNER JOIN sys.dm_exec_sessions blocking 
    ON blocked.blocking_session_id = blocking.session_id
CROSS APPLY sys.dm_exec_sql_text(blocked.sql_handle) sqltext
WHERE blocked.blocking_session_id > 0;
  1. Giải pháp:
    • Bật RCSI để readers không block writers
    • Tối ưu index để giảm thời gian lock
    • Rút ngắn transaction
    • Đúng isolation level cho use case
    • SET LOCK_TIMEOUT để fail fast thay vì chờ mãi

Q26: Chiến lược ngăn chặn Deadlock là gì?

A:

  1. Access objects theo cùng thứ tự: Nếu T1 và T2 đều lock Table A rồi Table B theo cùng thứ tự, deadlock không xảy ra.
  2. Giữ transaction ngắn: Giảm thời gian giữ locks.
  3. Thêm đúng indexes: Giảm số rows phải lock, tránh table scans.
  4. Dùng UPDLOCK hint: Lấy U lock từ đầu thay vì S -> X conversion.
  5. Dùng RCSI/SNAPSHOT: Readers không lấy S locks, loại bỏ Read-Write deadlocks.
  6. Retry logic: Viết application code bắt error 1205 và retry.

Q27: Giải thích Version Store trong tempdb và vấn đề tiềm ẩn.

A: Khi RCSI hoặc SNAPSHOT ISOLATION được bật, mỗi khi row được UPDATE, SQL Server ghi phiên bản cũ của row vào Version Store trong tempdb. Long-running transactions giữ versions này cho đến khi kết thúc, khiến tempdb phình to.

Monitor Version Store:

SELECT 
    SUM(version_store_reserved_page_count) * 8 / 1024 AS version_store_MB
FROM sys.dm_db_file_space_usage;

-- Xem oldest active transaction
SELECT 
    transaction_id,
    transaction_begin_time,
    DATEDIFF(MINUTE, transaction_begin_time, GETDATE()) AS minutes_open
FROM sys.dm_tran_active_snapshot_database_transactions
ORDER BY transaction_begin_time;

Giải pháp: Kết thúc long-running transactions, monitor tempdb growth, ensure tempdb autogrowth được cấu hình đúng.


Q28: Tại sao nên dùng sp_executesql thay vì EXEC với dynamic SQL trong context bảo mật transaction?

A:

-- BAD: Dễ bị SQL Injection
DECLARE @sql NVARCHAR(500) = 'SELECT * FROM Users WHERE Name = ''' + @input + '''';
EXEC(@sql);

-- GOOD: Parameterized, safe against SQL Injection
DECLARE @sql NVARCHAR(500) = 'SELECT * FROM Users WHERE Name = @name';
EXEC sp_executesql @sql, N'@name NVARCHAR(100)', @name = @input;

sp_executesql cũng giúp plan caching tốt hơn và an toàn trong nested transactions vì không tạo scope mới cho @@TRANCOUNT.


Q29: Giải thích cơ chế chọn Deadlock Victim.

A: SQL Server dùng thuật toán để chọn transaction nào sẽ bị kill (deadlock victim) dựa trên:

  1. DEADLOCK_PRIORITY: Transaction có priority thấp hơn bị chọn trước.
SET DEADLOCK_PRIORITY LOW; -- Tự nguyện làm victim
SET DEADLOCK_PRIORITY HIGH; -- Ít có khả năng làm victim
SET DEADLOCK_PRIORITY NORMAL; -- Default
  1. Transaction cost: Nếu priority bằng nhau, transaction nào tốn ít cost hơn để rollback (log records ít hơn) sẽ bị chọn.

  2. DEADLOCK_PRIORITY nhận giá trị từ -10 (LOWEST) đến 10 (HIGHEST).


Q30: Thiết kế retry mechanism cho Deadlock ở application level như thế nào?

A:

// C# example với Polly hoặc custom retry
public async Task ExecuteWithDeadlockRetry(Func<Task> action, int maxRetries = 3)
{
    int attempt = 0;
    while (true)
    {
        try
        {
            await action();
            return;
        }
        catch (SqlException ex) when (ex.Number == 1205) // Deadlock victim
        {
            attempt++;
            if (attempt >= maxRetries) throw;
            
            // Exponential backoff với jitter
            int delay = (int)(Math.Pow(2, attempt) * 100 + Random.Shared.Next(0, 100));
            await Task.Delay(delay);
        }
    }
}

Nguyên tắc: Retry deadlock là hợp lệ vì deadlock là transient error. Nhưng không retry lỗi business logic hay constraint violation.

Giao dịch (Transactions)

Transaction (Giao dịch) là đơn vị công việc logic gồm một hoặc nhiều thao tác được thực thi như một khối hoàn chỉnh — hoặc tất cả thành công, hoặc không có gì được thực hiện. Đây là nền tảng của tính toàn vẹn dữ liệu trong SQL Server.


1. ACID Properties

ACID là bốn thuộc tính cơ bản mà mọi transaction phải đảm bảo:

Atomicity (Tính nguyên tử)

“All or nothing” — Tất cả thao tác trong transaction thực hiện thành công toàn bộ, hoặc nếu bất kỳ thao tác nào thất bại, toàn bộ transaction bị ROLLBACK về trạng thái ban đầu.

Ví dụ thực tế: Chuyển tiền ngân hàng — trừ tiền tài khoản A và cộng tiền tài khoản B phải là một đơn vị. Không thể trừ mà không cộng.

BEGIN TRANSACTION;
    UPDATE BankAccounts SET Balance = Balance - 1000000 WHERE AccountId = 1;
    UPDATE BankAccounts SET Balance = Balance + 1000000 WHERE AccountId = 2;
    -- Nếu dòng trên fail, toàn bộ rollback
COMMIT TRANSACTION;

Consistency (Tính nhất quán)

Transaction phải đưa database từ trạng thái hợp lệ (valid state) này sang trạng thái hợp lệ khác. Tất cả constraints (PRIMARY KEY, FOREIGN KEY, CHECK, UNIQUE), triggers, và business rules phải được thỏa mãn sau khi transaction hoàn thành.

Ví dụ: Tổng số dư của tất cả tài khoản ngân hàng trước và sau khi chuyển tiền phải bằng nhau.

Isolation (Tính cô lập)

Các transaction đang thực thi đồng thời phải độc lập với nhau — transaction này không được nhìn thấy kết quả trung gian (chưa commit) của transaction khác. Mức độ cô lập được điều chỉnh qua Isolation Levels.

Durability (Tính bền vững)

Sau khi transaction được COMMIT, dữ liệu phải được lưu trữ vĩnh viễn kể cả khi hệ thống crash, mất điện. SQL Server đảm bảo điều này thông qua Write-Ahead Logging (WAL).


2. Transaction Syntax

BEGIN TRANSACTION

-- Tường minh
BEGIN TRANSACTION;
-- Hoặc viết tắt
BEGIN TRAN;

-- Đặt tên transaction (hỗ trợ nested transaction với savepoints)
BEGIN TRANSACTION TransferMoney;

COMMIT TRANSACTION

COMMIT TRANSACTION;
-- Hoặc
COMMIT TRAN;
-- Hoặc chỉ
COMMIT;

ROLLBACK TRANSACTION

ROLLBACK TRANSACTION;
-- Hoặc
ROLLBACK TRAN;
-- Hoặc
ROLLBACK;

Ví dụ hoàn chỉnh

BEGIN TRANSACTION;
BEGIN TRY
    INSERT INTO Orders (CustomerId, OrderDate, TotalAmount)
    VALUES (101, GETDATE(), 500000);

    DECLARE @OrderId INT = SCOPE_IDENTITY();

    INSERT INTO OrderDetails (OrderId, ProductId, Quantity, UnitPrice)
    VALUES (@OrderId, 5, 2, 250000);

    COMMIT TRANSACTION;
    PRINT 'Order created successfully';
END TRY
BEGIN CATCH
    IF @@TRANCOUNT > 0
        ROLLBACK TRANSACTION;
    PRINT 'Error: ' + ERROR_MESSAGE();
    THROW; -- Re-throw để caller biết
END CATCH;

3. @@TRANCOUNT — Đếm Transaction lồng nhau

@@TRANCOUNT trả về số lượng transaction đang mở trong session hiện tại:

  • BEGIN TRANSACTION@@TRANCOUNT + 1
  • COMMIT@@TRANCOUNT - 1 (chỉ commit khi về 0)
  • ROLLBACK@@TRANCOUNT = 0 (rollback toàn bộ, bất kể nested level)
PRINT @@TRANCOUNT; -- 0

BEGIN TRANSACTION; -- @@TRANCOUNT = 1
    BEGIN TRANSACTION; -- @@TRANCOUNT = 2
        UPDATE Products SET Price = 100 WHERE ProductId = 1;
    COMMIT; -- @@TRANCOUNT = 1 (chưa thực sự commit!)
COMMIT; -- @@TRANCOUNT = 0, NOW actually committed

-- Nguy hiểm: ROLLBACK ở bất kỳ level nào cũng rollback toàn bộ
BEGIN TRANSACTION; -- @@TRANCOUNT = 1
    BEGIN TRANSACTION; -- @@TRANCOUNT = 2
        BEGIN TRANSACTION; -- @@TRANCOUNT = 3
        ROLLBACK; -- @@TRANCOUNT = 0, TOÀN BỘ bị rollback!

Lưu ý quan trọng: Trong stored procedures, nên kiểm tra @@TRANCOUNT để tránh rollback nhầm outer transaction:

CREATE PROCEDURE dbo.SafeInsert
AS
BEGIN
    DECLARE @TranCount INT = @@TRANCOUNT;
    
    IF @TranCount = 0
        BEGIN TRANSACTION; -- Chỉ mở nếu chưa có transaction
    ELSE
        SAVE TRANSACTION SavePoint1; -- Dùng savepoint nếu đã trong transaction
    
    BEGIN TRY
        -- Business logic
        INSERT INTO ...
        
        IF @TranCount = 0
            COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        IF @TranCount = 0
            ROLLBACK TRANSACTION;
        ELSE
            ROLLBACK TRANSACTION SavePoint1;
        THROW;
    END CATCH;
END;

4. Savepoints

Savepoints cho phép đặt “điểm lưu” bên trong transaction để có thể rollback về điểm đó mà không rollback toàn bộ transaction.

BEGIN TRANSACTION;
    INSERT INTO AuditLog VALUES ('Process started', GETDATE());
    
    SAVE TRANSACTION BeforeOrder; -- Đặt savepoint
    
    BEGIN TRY
        INSERT INTO Orders (CustomerId) VALUES (999); -- 999 có thể không tồn tại
        SAVE TRANSACTION AfterOrder;
        
        INSERT INTO OrderDetails (OrderId, ProductId) VALUES (SCOPE_IDENTITY(), 1);
    END TRY
    BEGIN CATCH
        IF ERROR_NUMBER() = 547 -- Foreign key violation
        BEGIN
            ROLLBACK TRANSACTION BeforeOrder; -- Chỉ rollback đến savepoint
            -- AuditLog INSERT vẫn còn!
            INSERT INTO AuditLog VALUES ('Order failed: FK violation', GETDATE());
        END
        ELSE
            THROW;
    END CATCH;
    
    INSERT INTO AuditLog VALUES ('Process completed', GETDATE());
COMMIT TRANSACTION; -- Commit tất cả những gì còn lại

Giới hạn của Savepoints:

  • ROLLBACK TO SAVEPOINT không giảm @@TRANCOUNT
  • Không thể dùng savepoint để commit một phần — chỉ có thể rollback về điểm đó
  • Không hỗ trợ trong Distributed Transactions

5. Implicit vs Explicit Transactions

Explicit Transactions (Mặc định)

Mọi statement là auto-commit trừ khi bạn tường minh BEGIN TRANSACTION.

-- Auto-commit mode (mặc định):
INSERT INTO Table1 VALUES (1); -- Commit ngay lập tức
UPDATE Table2 SET Col = 1 WHERE Id = 1; -- Commit ngay lập tức

-- Explicit:
BEGIN TRANSACTION;
    INSERT INTO Table1 VALUES (1);
    UPDATE Table2 SET Col = 1 WHERE Id = 1;
COMMIT; -- Cả hai commit cùng lúc

Implicit Transactions

Khi SET IMPLICIT_TRANSACTIONS ON, SQL Server tự động mở transaction trước các DML/DDL statements. Bạn phải tường minh COMMIT hoặc ROLLBACK.

SET IMPLICIT_TRANSACTIONS ON;

INSERT INTO Products VALUES ('Widget', 100); -- SQL tự BEGIN TRAN
SELECT * FROM Products; -- Vẫn trong transaction
COMMIT; -- Bạn phải commit tường minh

-- Tiếp tục, SQL tự mở transaction mới
UPDATE Products SET Price = 200 WHERE ProductId = 1;
ROLLBACK; -- Rollback update

Cảnh báo: IMPLICIT_TRANSACTIONS ON thường gây ra vấn đề — DBA hay developer quên COMMIT gây ra long-running transactions. Không nên dùng trong ứng dụng production.


6. Auto-commit Mode

SQL Server mặc định chạy ở auto-commit mode: mỗi statement là một transaction riêng biệt, tự động commit khi thành công hoặc rollback khi thất bại.

-- Không có BEGIN TRAN:
INSERT INTO T1 VALUES (1); -- Tự động commit
UPDATE T2 SET C = 1; -- Tự động commit (riêng biệt)
DELETE FROM T3 WHERE Id = 5; -- Tự động commit (riêng biệt)

-- Nếu UPDATE fail vì constraint, chỉ UPDATE bị rollback, INSERT vẫn commit

7. Transaction Log & Write-Ahead Logging (WAL)

Transaction Log

SQL Server dùng Transaction Log (.ldf file) để ghi lại tất cả thay đổi dữ liệu trước khi áp dụng lên data pages.

Cấu trúc:

  • Log file chia thành Virtual Log Files (VLFs)
  • Mỗi VLF chứa nhiều Log Records
  • Log records được ghi tuần tự (sequential I/O — nhanh hơn random I/O)

Write-Ahead Logging (WAL)

Quy tắc WAL: Log record phải được ghi vào disk trước khi data page tương ứng được ghi xuống disk.

Luồng hoạt động:

1. Transaction bắt đầu → SQL Server tạo log record "BEGIN TRAN"
2. UPDATE row → Ghi log record (Before Image + After Image) vào Log Buffer
3. COMMIT → SQL Server flush Log Buffer xuống disk (log flush)
4. COMMIT hoàn thành → User nhận được kết quả
5. (Nền) Checkpoint → Data pages được ghi xuống disk

Checkpoint

Checkpoint là quá trình ghi tất cả dirty pages (pages đã modify trong memory nhưng chưa ghi xuống disk) xuống disk:

-- Manual checkpoint
CHECKPOINT;

-- Xem thời gian recovery estimate
SELECT recovery_model_desc, log_reuse_wait_desc
FROM sys.databases WHERE name = DB_NAME();

Recovery sau crash:

  1. REDO (Roll forward): Áp lại tất cả log records của committed transactions sau checkpoint
  2. UNDO (Roll back): Hủy tất cả log records của uncommitted transactions

8. Long-running Transactions — Tác hại và Xử lý

Tác hại

1. Transaction Log không thể truncate (reuse)
   → Log file phình to, hết disk space
   
2. Giữ locks trong thời gian dài
   → Blocking nhiều session khác
   
3. Version Store bloat (nếu dùng RCSI/Snapshot)
   → tempdb phình to
   
4. Rollback time = Run time
   → Transaction chạy 2h có thể rollback 2h

Phát hiện Long-running Transactions

-- Tìm transactions đang chạy lâu
SELECT 
    ses.session_id,
    ses.login_name,
    ses.host_name,
    ses.status,
    req.command,
    req.wait_type,
    req.wait_time / 1000 AS wait_seconds,
    DATEDIFF(MINUTE, req.start_time, GETDATE()) AS running_minutes,
    tran.open_transaction_count,
    SUBSTRING(sql.text, (req.statement_start_offset/2)+1, 200) AS current_statement
FROM sys.dm_exec_sessions ses
INNER JOIN sys.dm_exec_requests req ON ses.session_id = req.session_id
INNER JOIN sys.dm_exec_session_wait_stats tran ON ses.session_id = tran.session_id
CROSS APPLY sys.dm_exec_sql_text(req.sql_handle) sql
WHERE req.open_transaction_count > 0
ORDER BY running_minutes DESC;

-- Kiểm tra oldest active transaction
SELECT 
    transaction_id,
    transaction_begin_time,
    DATEDIFF(MINUTE, transaction_begin_time, GETDATE()) AS minutes_open,
    name AS transaction_name
FROM sys.dm_tran_active_transactions
WHERE transaction_type = 1 -- User transaction
ORDER BY transaction_begin_time;

Best Practices xử lý long-running transactions

-- Thay vì một transaction lớn xóa 1 triệu rows:
-- BAD:
BEGIN TRANSACTION;
DELETE FROM OldLogs WHERE LogDate < '2020-01-01'; -- 1 triệu rows
COMMIT;

-- GOOD: Chia thành batches
DECLARE @BatchSize INT = 10000;
DECLARE @Deleted INT = 1;

WHILE @Deleted > 0
BEGIN
    DELETE TOP (@BatchSize) FROM OldLogs WHERE LogDate < '2020-01-01';
    SET @Deleted = @@ROWCOUNT;
    PRINT 'Deleted ' + CAST(@Deleted AS VARCHAR) + ' rows';
    -- Nhường CPU/IO cho session khác
    WAITFOR DELAY '00:00:01';
END;

9. Distributed Transactions & MSDTC

Distributed Transaction span qua nhiều SQL Server instances hoặc resource managers khác nhau.

Microsoft Distributed Transaction Coordinator (MSDTC)

MSDTC điều phối 2-Phase Commit Protocol:

  1. Prepare Phase: MSDTC hỏi tất cả participants “bạn sẵn sàng commit chưa?”
  2. Commit/Rollback Phase: Nếu tất cả OK → Commit. Một người fail → tất cả Rollback.
-- Explicit distributed transaction
BEGIN DISTRIBUTED TRANSACTION;
    -- Update trên server local
    UPDATE LocalDB.dbo.Inventory 
    SET Quantity = Quantity - 10 
    WHERE ProductId = 1;

    -- Update trên linked server (remote)
    UPDATE [RemoteWarehouse].WMS.dbo.Stock 
    SET OnHand = OnHand - 10 
    WHERE ProductId = 1;

COMMIT;

Khi nào tự động trở thành Distributed Transaction

-- SQL Server tự động promote thành distributed transaction khi:
-- 1. Query qua Linked Server trong transaction hiện tại
BEGIN TRANSACTION;
    UPDATE LocalTable SET ...; -- Local transaction
    SELECT * FROM [LinkedServer].RemoteDB.dbo.Table; -- Tự động promote!
COMMIT;

Vấn đề với MSDTC:

  • Latency cao (network round trips cho handshake)
  • MSDTC là single point of failure
  • Khó debug khi có sự cố
  • Modern architecture (microservices) thường dùng Saga Pattern với compensating transactions thay thế.

10. Error Handling với Transactions

Pattern chuẩn: TRY-CATCH-ROLLBACK

SET XACT_ABORT ON; -- Best practice: tự động rollback khi có lỗi
BEGIN TRANSACTION;
BEGIN TRY
    -- Step 1: Validate
    IF NOT EXISTS (SELECT 1 FROM Customers WHERE CustomerId = @CustomerId)
        THROW 50001, 'Customer not found', 1;

    -- Step 2: Create Order
    INSERT INTO Orders (CustomerId, CreatedAt)
    VALUES (@CustomerId, GETDATE());
    
    DECLARE @OrderId INT = SCOPE_IDENTITY();

    -- Step 3: Add Order Lines
    INSERT INTO OrderLines (OrderId, ProductId, Qty)
    SELECT @OrderId, ProductId, Qty
    FROM @OrderItems;

    -- Step 4: Update Inventory
    UPDATE Products 
    SET Stock = Stock - ol.Qty
    FROM Products p
    INNER JOIN OrderLines ol ON p.ProductId = ol.ProductId
    WHERE ol.OrderId = @OrderId;

    -- Commit nếu tất cả OK
    COMMIT TRANSACTION;
    SELECT @OrderId AS NewOrderId;
END TRY
BEGIN CATCH
    -- Rollback nếu có lỗi bất kỳ
    IF @@TRANCOUNT > 0
        ROLLBACK TRANSACTION;
    
    -- Log error
    INSERT INTO ErrorLog (ErrorNumber, ErrorMessage, ErrorLine, ErrorTime)
    VALUES (ERROR_NUMBER(), ERROR_MESSAGE(), ERROR_LINE(), GETDATE());
    
    -- Re-throw để caller xử lý
    THROW;
END CATCH;

XACT_ABORT và ảnh hưởng

-- Không có XACT_ABORT:
BEGIN TRANSACTION;
    INSERT INTO T1 VALUES (1); -- OK
    INSERT INTO T1 VALUES (1); -- Duplicate KEY error → chỉ statement này fail
    INSERT INTO T1 VALUES (2); -- Statement này vẫn chạy!
COMMIT; -- @@TRANCOUNT = 1, commit T1=1 và T1=2 (không có duplicate)

-- Với XACT_ABORT ON:
SET XACT_ABORT ON;
BEGIN TRANSACTION;
    INSERT INTO T1 VALUES (1); -- OK
    INSERT INTO T1 VALUES (1); -- Error → TOÀN BỘ transaction bị rollback, @@TRANCOUNT = 0
    INSERT INTO T1 VALUES (2); -- Không được chạy
-- @@TRANCOUNT đã = 0, không cần ROLLBACK

XACT_STATE() — kiểm tra trạng thái transaction trong CATCH block:

BEGIN CATCH
    DECLARE @XactState INT = XACT_STATE();
    IF @XactState = -1
        ROLLBACK; -- Transaction không thể commit, PHẢI rollback
    ELSE IF @XactState = 1
        COMMIT; -- Transaction có thể commit (uncommittable = false)
    -- @XactState = 0: Không có transaction đang mở
END CATCH;

11. Best Practices

✅ Nên làm

-- 1. Giữ transaction càng ngắn càng tốt
BEGIN TRANSACTION;
    -- Chỉ DML statements cần thiết
    UPDATE Orders SET Status = 'Processed' WHERE OrderId = @Id;
    INSERT INTO Audit VALUES (@Id, GETDATE());
COMMIT;

-- 2. Luôn dùng SET XACT_ABORT ON trong stored procedures
CREATE PROCEDURE dbo.ProcessOrder @OrderId INT
AS
BEGIN
    SET XACT_ABORT ON;
    SET NOCOUNT ON;
    BEGIN TRANSACTION;
    -- ...
    COMMIT;
END;

-- 3. Kiểm tra @@TRANCOUNT trước ROLLBACK
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;

-- 4. Dùng THROW thay vì RAISERROR để re-throw
BEGIN CATCH
    IF @@TRANCOUNT > 0 ROLLBACK;
    THROW; -- Giữ nguyên error info gốc
END CATCH;

❌ Không nên làm

-- 1. Tương tác người dùng bên trong transaction
BEGIN TRANSACTION;
    UPDATE Products SET Price = @NewPrice WHERE ProductId = @Id;
    -- ĐỪNG: Chờ user confirm trước khi commit
    -- Lock giữ trong khi chờ → blocking!
COMMIT;

-- 2. Transaction quá lớn
BEGIN TRANSACTION;
    DELETE FROM Logs WHERE CreatedAt < '2020-01-01'; -- 10 triệu rows!
COMMIT;
-- → Log file phình to, block other sessions, rollback time lâu

-- 3. Quên ROLLBACK trong error handling
BEGIN TRANSACTION;
    UPDATE ...;
    IF @@ERROR <> 0
        RETURN; -- NGUY HIỂM: Transaction vẫn đang mở!
COMMIT;

-- 4. Dùng SELECT ... NOLOCK trong financial queries
SELECT SUM(Balance) FROM Accounts WITH (NOLOCK); -- Có thể sai vì dirty read!

Tóm tắt nhanh

Khái niệmMô tả
BEGIN TRANMở transaction tường minh
COMMITXác nhận và lưu vĩnh viễn
ROLLBACKHủy toàn bộ transaction
SAVE TRAN nameĐặt savepoint để rollback một phần
@@TRANCOUNTSố transactions đang mở
XACT_ABORT ONTự động rollback khi có lỗi
XACT_STATE()Trạng thái transaction trong CATCH
WALWrite-Ahead Logging — đảm bảo Durability
CheckpointFlush dirty pages xuống disk
MSDTCDistributed transaction coordinator

Isolation Levels

Isolation Levels (Mức độ cô lập) xác định mức độ mà một transaction phải bị cô lập khỏi các thao tác đọc/ghi của các transaction đồng thời khác. Chọn đúng isolation level là sự cân bằng giữa tính nhất quán dữ liệuhiệu suất đồng thời (concurrency).


1. Các vấn đề Concurrency (Tại sao cần Isolation Levels?)

Dirty Read

Transaction đọc dữ liệu mà transaction khác đang sửa nhưng chưa COMMIT. Nếu transaction kia ROLLBACK, dữ liệu đã đọc là không tồn tại.

-- Session 1: Chưa commit
BEGIN TRANSACTION;
UPDATE Products SET Price = 999 WHERE ProductId = 1; -- Giá thực: 500
-- Chưa COMMIT...

-- Session 2 (READ UNCOMMITTED): Đọc được 999 — Dirty Read!
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT Price FROM Products WHERE ProductId = 1; -- Trả về 999 (SAI!)

-- Session 1: Rollback → giá thực vẫn là 500
ROLLBACK;

Non-repeatable Read

Cùng một row được đọc hai lần trong cùng transaction nhưng cho kết quả khác nhau vì transaction khác đã UPDATE/DELETE row đó giữa hai lần đọc.

-- Session 1 (READ COMMITTED):
BEGIN TRANSACTION;
SELECT Price FROM Products WHERE ProductId = 1; -- Lần 1: 500

-- Session 2: Commit update
UPDATE Products SET Price = 800 WHERE ProductId = 1;
COMMIT;

-- Session 1: Đọc lại (vẫn trong cùng transaction)
SELECT Price FROM Products WHERE ProductId = 1; -- Lần 2: 800 → Non-repeatable Read!
ROLLBACK;

Phantom Read

Cùng một query (với WHERE condition) được chạy hai lần nhưng trả về số rows khác nhau vì transaction khác đã INSERT/DELETE rows phù hợp với điều kiện WHERE giữa hai lần chạy.

-- Session 1 (REPEATABLE READ):
BEGIN TRANSACTION;
SELECT * FROM Orders WHERE Amount > 1000000; -- Lần 1: 5 rows

-- Session 2: Thêm order mới
INSERT INTO Orders (CustomerId, Amount) VALUES (1, 2000000);
COMMIT;

-- Session 1: Đọc lại
SELECT * FROM Orders WHERE Amount > 1000000; -- Lần 2: 6 rows → Phantom Read!
ROLLBACK;

Lost Update

Hai transactions cùng đọc một giá trị, sau đó cùng update dựa trên giá trị đọc được — một trong hai update bị ghi đè (overwrite) mất.

-- Cả hai cùng đọc: Quantity = 100
-- Session 1: UPDATE Products SET Quantity = 100 - 10 = 90 WHERE Id = 1;
-- Session 2: UPDATE Products SET Quantity = 100 - 15 = 85 WHERE Id = 1;
-- Kết quả cuối: 85 (mất đi việc trừ 10 của Session 1!) → Lost Update

2. Ma trận Isolation Levels vs Concurrency Problems

Isolation LevelDirty ReadNon-repeatable ReadPhantom ReadLost Update
READ UNCOMMITTED✅ Có thể✅ Có thể✅ Có thể✅ Có thể
READ COMMITTED❌ Ngăn✅ Có thể✅ Có thể✅ Có thể
REPEATABLE READ❌ Ngăn❌ Ngăn✅ Có thể❌ Ngăn
SERIALIZABLE❌ Ngăn❌ Ngăn❌ Ngăn❌ Ngăn
RCSI❌ Ngăn✅ Có thể*✅ Có thể✅ Có thể
SNAPSHOT❌ Ngăn❌ Ngăn❌ Ngăn❌ Ngăn**

* RCSI đọc snapshot tại thời điểm statement bắt đầu — không phải transaction bắt đầu.
** SNAPSHOT phát hiện write conflicts và fail transaction.


3. READ UNCOMMITTED — Mức thấp nhất

Cơ chế: Không lấy bất kỳ shared lock nào khi đọc. Đọc được mọi dữ liệu kể cả chưa commit.

Cho phép: Dirty Read, Non-repeatable Read, Phantom Read.

-- Cách 1: Set cho toàn session
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT * FROM Orders;

-- Cách 2: NOLOCK hint (tương đương READ UNCOMMITTED cho query đó)
SELECT * FROM Orders WITH (NOLOCK);

-- Cách 3: READUNCOMMITTED hint
SELECT * FROM Orders WITH (READUNCOMMITTED);

Khi nào dùng: Báo cáo approximate (ước tính) cần tốc độ, chấp nhận số liệu không chính xác tuyệt đối. Ví dụ: Dashboard hiển thị “khoảng X đơn hàng đang xử lý”.

⚠️ Cảnh báo nghiêm trọng với NOLOCK:

  • Đọc được dữ liệu sẽ bị rollback (Dirty Read)
  • Có thể skip rows (bỏ sót rows) khi page split xảy ra
  • Có thể đọc trùng rows (duplicate rows)
  • KHÔNG BAO GIỜ dùng trong: báo cáo tài chính, tính toán tổng/số lượng cần chính xác, business logic quan trọng.

4. READ COMMITTED — Mặc định của SQL Server

Cơ chế: Lấy shared lock khi đọc, giải phóng ngay khi đọc xong (statement-level). Chỉ đọc dữ liệu đã COMMIT.

Ngăn chặn: Dirty Read.
Cho phép: Non-repeatable Read, Phantom Read.

SET TRANSACTION ISOLATION LEVEL READ COMMITTED; -- Đây là mặc định

BEGIN TRANSACTION;
    SELECT Price FROM Products WHERE ProductId = 1; -- 500, lấy S lock rồi release
    
    -- Trong lúc này, session khác CÓ THỂ update và commit!
    
    SELECT Price FROM Products WHERE ProductId = 1; -- Có thể là 800 → Non-repeatable Read
COMMIT;

Ưu điểm: Cân bằng tốt giữa consistency và concurrency. Phù hợp cho hầu hết OLTP scenarios.
Nhược điểm: Readers block writers (và ngược lại) → giải quyết bằng RCSI.


5. REPEATABLE READ

Cơ chế: Giữ shared lock cho đến khi transaction kết thúc. Đảm bảo cùng một row đọc nhiều lần sẽ cho kết quả giống nhau.

Ngăn chặn: Dirty Read, Non-repeatable Read, Lost Update.
Cho phép: Phantom Read.

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

BEGIN TRANSACTION;
    SELECT Price FROM Products WHERE ProductId = 1; -- 500, giữ S lock
    
    -- Session khác muốn UPDATE ProductId = 1 sẽ bị BLOCK!
    -- S lock giữ cho đến COMMIT
    
    SELECT Price FROM Products WHERE ProductId = 1; -- LUÔN LUÔN là 500
    
    -- Nhưng INSERT rows mới vẫn có thể xảy ra → Phantom Read vẫn được
COMMIT; -- Giải phóng tất cả S locks

Khi nào dùng: Khi cần đọc cùng dữ liệu nhiều lần trong cùng transaction và phải nhất quán (ví dụ: tính toán phức tạp trên cùng tập dữ liệu).
Nhược điểm: Giảm concurrency đáng kể vì giữ locks lâu.


6. SERIALIZABLE — Mức cao nhất

Cơ chế: Thêm key-range locks vào REPEATABLE READ. Ngăn chặn INSERT vào ranges mà transaction đang query.

Ngăn chặn: Tất cả — Dirty Read, Non-repeatable Read, Phantom Read, Lost Update.
Cho phép: Không có concurrency anomaly nào.

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

BEGIN TRANSACTION;
    -- Query này lock cả key-range Orders WHERE Amount > 1000000
    SELECT * FROM Orders WHERE Amount > 1000000; -- 5 rows, lock key range
    
    -- Session khác muốn INSERT Orders (Amount = 2000000) sẽ bị BLOCK!
    
    SELECT * FROM Orders WHERE Amount > 1000000; -- LUÔN LUÔN là 5 rows
COMMIT;

Khi nào dùng: Giao dịch tài chính cần độ chính xác tuyệt đối, kiểm kho (inventory check + reserve), phân bổ số thứ tự (allocation).
Nhược điểm: Giảm concurrency nhất, dễ gây deadlock nhất.


7. READ COMMITTED SNAPSHOT ISOLATION (RCSI)

RCSI là phiên bản “optimistic” của READ COMMITTED, dùng row versioning thay vì shared locks.

Cách hoạt động

Khi a row bị UPDATE, SQL Server:

  1. Ghi phiên bản cũ của row vào Version Store trong tempdb
  2. Row trên data page được update với version mới

Khi một reader muốn đọc, thay vì chờ lock → reader đọc committed version gần nhất từ Version Store (không cần lock!).

-- Bật RCSI cho database (phải không có connection nào đang dùng)
ALTER DATABASE MyDatabase SET READ_COMMITTED_SNAPSHOT ON WITH NO_WAIT;

-- Kiểm tra
SELECT name, is_read_committed_snapshot_on
FROM sys.databases WHERE name = 'MyDatabase';

-- Khi đã bật, không cần thay đổi code:
-- Tất cả READ COMMITTED queries tự động dùng row versioning
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; -- Vẫn dùng level này
SELECT * FROM Orders; -- Nhưng không block, không bị block

So sánh READ COMMITTED vs RCSI

-- Scenario: Session 1 UPDATE chưa commit, Session 2 SELECT

-- Với READ COMMITTED (không có RCSI):
-- Session 1: BEGIN TRAN; UPDATE Orders SET Status = 'Processed' WHERE Id = 1;
-- Session 2: SELECT * FROM Orders WHERE Id = 1; → BLOCKED! Phải chờ Session 1

-- Với RCSI bật:
-- Session 1: BEGIN TRAN; UPDATE Orders SET Status = 'Processed' WHERE Id = 1;
-- Session 2: SELECT * FROM Orders WHERE Id = 1; → Đọc ngay phiên bản cũ (committed)!
-- Không bị block!

Đặc điểm của RCSI

  • Statement-level snapshot: Mỗi statement thấy committed data tại thời điểm statement bắt đầu
  • Writers không block Readers và ngược lại
  • Overhead: tempdb phải lưu version store → tempdb lớn hơn
  • Mặc định trong Azure SQL Database: Luôn bật

8. SNAPSHOT ISOLATION

SNAPSHOT ISOLATION là transaction-level snapshot — toàn bộ transaction thấy cùng một snapshot từ thời điểm transaction bắt đầu (không phải statement bắt đầu).

-- Step 1: Cho phép Snapshot Isolation ở database level
ALTER DATABASE MyDatabase SET ALLOW_SNAPSHOT_ISOLATION ON;

-- Step 2: Session sử dụng
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;

BEGIN TRANSACTION; -- Snapshot được tạo tại thời điểm này!
    -- Mọi SELECT trong transaction này thấy data tại thời điểm BEGIN TRAN
    SELECT * FROM Orders WHERE CustomerId = 1; -- Snapshot tại T0

    -- Dù session khác commit update, bạn vẫn thấy snapshot T0
    
    SELECT * FROM Orders WHERE CustomerId = 1; -- Vẫn là snapshot T0 → Repeatable!
COMMIT;

Write Conflict Detection — Điểm khác biệt với RCSI

SET TRANSACTION ISOLATION LEVEL SNAPSHOT;

BEGIN TRANSACTION; -- Snapshot tại T0, Orders.Status = 'Pending'

    -- Session khác: UPDATE Orders SET Status = 'Cancelled' WHERE OrderId = 1; COMMIT;
    
    -- Bạn cố UPDATE cùng row:
    UPDATE Orders SET Status = 'Processed' WHERE OrderId = 1;
    -- → LỖI! Error 3960: Snapshot isolation transaction aborted due to update conflict.
    -- SNAPSHOT phát hiện row đã bị modify sau khi transaction bắt đầu
COMMIT;

Đây là Optimistic Concurrency: Assume không có conflict, nhưng check khi commit. Phù hợp khi conflicts hiếm.


9. Row Versioning — Cơ chế nền tảng

Cả RCSI và SNAPSHOT ISOLATION đều dùng Row Versioning Store trong tempdb:

Data Page (data file):
┌─────────────────────────────────────────────┐
│ Row: OrderId=1, Amount=500, [Version Ptr] ───┼──► tempdb Version Store
└─────────────────────────────────────────────┘

tempdb Version Store:
┌─────────────────────────────────────────────┐
│ Version 1: OrderId=1, Amount=500 (original) │ ← Reader thấy cái này
│ Version 2: OrderId=1, Amount=800 (new)      │ ← Writer đang update
└─────────────────────────────────────────────┘

Monitor Version Store:

-- Xem kích thước version store
SELECT 
    DB_NAME(database_id) AS DatabaseName,
    SUM(version_store_reserved_page_count) * 8 AS version_store_KB
FROM sys.dm_db_file_space_usage
GROUP BY database_id
ORDER BY version_store_KB DESC;

-- Xem oldest active snapshot transaction (giữ versions lâu nhất)
SELECT 
    transaction_id,
    transaction_begin_time,
    DATEDIFF(SECOND, transaction_begin_time, GETDATE()) AS seconds_open,
    elapsed_time_seconds
FROM sys.dm_tran_active_snapshot_database_transactions
ORDER BY transaction_begin_time;

10. Đặt Isolation Level

Session Level

-- Cài đặt cho toàn bộ session từ đây trở đi
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;    -- Mặc định
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;           -- Cần ALLOW_SNAPSHOT_ISOLATION ON

Query Level (Table Hints)

-- Read Uncommitted (Dirty Read OK)
SELECT * FROM Orders WITH (NOLOCK);
SELECT * FROM Orders WITH (READUNCOMMITTED);

-- Read Committed (default behavior)
SELECT * FROM Orders WITH (READCOMMITTED);

-- Repeatable Read
SELECT * FROM Orders WITH (REPEATABLEREAD);

-- Serializable
SELECT * FROM Orders WITH (SERIALIZABLE);
SELECT * FROM Orders WITH (HOLDLOCK); -- Tương đương SERIALIZABLE

-- Update Lock (dùng khi chuẩn bị UPDATE)
SELECT * FROM Orders WITH (UPDLOCK) WHERE OrderId = 1;

-- Serializable + Update Lock
SELECT * FROM Orders WITH (UPDLOCK, HOLDLOCK) WHERE OrderId = 1;

11. Performance Implications

Locking Approach (READ COMMITTED, REPEATABLE READ, SERIALIZABLE)

Ưu điểm:
+ Không overhead để ghi version history
+ Đơn giản, ít phức tạp

Nhược điểm:
- Readers block Writers
- Writers block Readers  
- Deadlock risk cao hơn
- Latency cao hơn trong môi trường contention cao

Row Versioning Approach (RCSI, SNAPSHOT)

Ưu điểm:
+ Readers không block Writers (và ngược lại)
+ Latency thấp hơn khi có contention
+ Ít deadlock hơn (read-write deadlocks không xảy ra)

Nhược điểm:
- tempdb overhead (version store phải duy trì)
- Long-running transactions → version store phình to
- Slight overhead để đọc từ version chain
- Write conflicts trong SNAPSHOT có thể cần retry logic

Benchmark guidelines

-- Kiểm tra version cleanup performance
SELECT 
    version_store_reserved_page_count * 8.0 / 1024 AS version_store_MB,
    version_cleanup_rate_kb_per_s
FROM sys.dm_os_performance_counters
WHERE object_name LIKE '%Transactions%'
AND counter_name IN ('Version Store Size (KB)', 'Version Cleanup rate (KB/s)');

12. Chọn Isolation Level đúng

OLTP (Online Transaction Processing)

-- Use Case: Đặt hàng, thanh toán
-- Dùng: READ COMMITTED + RCSI (best practice)
-- Lý do: Readers không block writers, ít deadlock, consistent reads

-- Setup database:
ALTER DATABASE OrderDB SET READ_COMMITTED_SNAPSHOT ON;
-- Không cần thay đổi code, tự động áp dụng

Báo cáo / Analytics trên Production DB

-- Use Case: Report cần chạy trên live DB, chấp nhận dữ liệu hơi cũ
-- Dùng: SNAPSHOT ISOLATION hoặc query on replica
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
BEGIN TRANSACTION;
    -- Report queries - thấy consistent snapshot tại thời điểm bắt đầu
    SELECT SUM(Amount) FROM Orders WHERE OrderDate = CAST(GETDATE() AS DATE);
    SELECT COUNT(*) FROM Customers WHERE CreatedAt > DATEADD(DAY, -30, GETDATE());
COMMIT;

Tài chính / Tồn kho cần chính xác tuyệt đối

-- Use Case: Kiểm tra và giữ tồn kho trước khi bán
-- Dùng: SERIALIZABLE hoặc REPEATABLE READ + UPDLOCK hint
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;
    -- Check tồn kho
    SELECT Stock FROM Products WHERE ProductId = @ProductId;
    
    -- Trong serializable: không có INSERT/UPDATE nào có thể chen vào!
    
    IF (SELECT Stock FROM Products WHERE ProductId = @ProductId) >= @Quantity
    BEGIN
        UPDATE Products SET Stock = Stock - @Quantity WHERE ProductId = @ProductId;
        INSERT INTO Orders ...;
    END
COMMIT;

-- Hoặc dùng UPDLOCK hint để ít restrictive hơn:
BEGIN TRANSACTION;
    SELECT Stock 
    FROM Products WITH (UPDLOCK, ROWLOCK) -- Ngăn concurrent updates
    WHERE ProductId = @ProductId;
    
    UPDATE Products SET Stock = Stock - @Quantity WHERE ProductId = @ProductId;
COMMIT;

Kiểm tra nhanh không cần chính xác 100%

-- Use Case: Dashboard monitoring, approximate counts
-- Dùng: READ UNCOMMITTED / NOLOCK
SELECT 
    COUNT(*) AS ApproxPendingOrders
FROM Orders WITH (NOLOCK)
WHERE Status = 'Pending';

Tóm tắt

LevelCơ chếPhù hợp khi
READ UNCOMMITTEDNo locksApproximate monitoring, speed critical
READ COMMITTEDStatement-level S locksOLTP thông thường (default)
REPEATABLE READTransaction-level S locksCần re-read consistency
SERIALIZABLEKey-range locksTài chính, inventory check
RCSIRow versioning (statement)OLTP high concurrency (best default)
SNAPSHOTRow versioning (transaction)Reporting on live DB, optimistic concurrency

Locking, Blocking & Deadlocks

SQL Server dùng locking mechanism để kiểm soát truy cập đồng thời vào dữ liệu, đảm bảo tính toàn vẹn của transactions. Hiểu rõ cơ chế locking là yêu cầu bắt buộc để diagnose và giải quyết các vấn đề hiệu suất trong production.


1. Các loại Lock (Lock Types)

Shared Lock (S) — Đọc

  • Lấy khi thực hiện SELECT
  • Nhiều Shared lock có thể tồn tại đồng thời trên cùng resource
  • Không compatible với Exclusive lock
  • Trong READ COMMITTED: giải phóng sau khi đọc xong statement
  • Trong REPEATABLE READ/SERIALIZABLE: giữ đến khi transaction kết thúc

Exclusive Lock (X) — Ghi

  • Lấy khi thực hiện INSERT, UPDATE, DELETE
  • Không compatible với bất kỳ lock nào khác
  • Chỉ một X lock tại một thời điểm
  • Giữ đến khi transaction kết thúc (COMMIT/ROLLBACK)

Update Lock (U) — Chuẩn bị Update

  • Bước trung gian khi SQL Server scan để tìm rows cần UPDATE
  • Compatible với S lock (nhiều readers có thể đọc trong khi U scan)
  • Không compatible với U lock khác (ngăn chặn deadlock U→X)
  • Sẽ convert sang X lock khi thực sự modify row

Intent Locks — Cấp thấp hơn thông báo ý định

Intent locks được đặt ở cấp Page/Table để thông báo lock intention ở cấp thấp hơn (Row), giúp SQL Server kiểm tra nhanh compatibility mà không cần scan từng row:

Intent LockÝ nghĩa
IS (Intent Shared)Sẽ lấy S lock trên một row/page bên trong
IX (Intent Exclusive)Sẽ lấy X lock trên một row/page bên trong
SIX (Shared with Intent Exclusive)Giữ S lock toàn bộ, sẽ lấy X lock trên một số row
IU (Intent Update)Sẽ lấy U lock bên trong (hiếm gặp)
UIX (Update with Intent Exclusive)Giữ U lock, sẽ convert X

Schema Locks

LockTênKhi nào
Sch-MSchema ModificationALTER TABLE, DROP TABLE, rebuild index
Sch-SSchema StabilityCompile query plan — không block DML

Bulk Update Lock (BU)

Dùng trong BULK INSERT với TABLOCK hint — cho phép nhiều bulk insert đồng thời trên cùng table.

Key-Range Lock (SERIALIZABLE only)

Khóa khoảng giá trị trong index để ngăn Phantom Read:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;
SELECT * FROM Orders WHERE Amount BETWEEN 1000 AND 5000;
-- Locks key range [1000, 5000] — ngăn INSERT vào range này

2. Lock Compatibility Matrix

Bảng tương thích giữa các loại lock (✅ = Compatible, ❌ = Conflict):

Hiện có ↓ \ Yêu cầu →ISSUIXSIXX
IS
S
U
IX
SIX
X

3. Lock Granularity & Lock Escalation

Hierarchy của lock granularity (từ nhỏ đến lớn)

Database → File → Filegroup → Table → Extent → Page → Row (RID/KEY)

SQL Server cố gắng lock ở level nhỏ nhất (row-level) để maximize concurrency.

Lock Escalation

Khi số lượng row/page locks trong một statement vượt ngưỡng (~5,000 locks), SQL Server escalate (nâng cấp) lên table lock để tiết kiệm memory lock manager.

5,000 row locks × (64 bytes/lock) = 320KB bộ nhớ → Escalate!

Vấn đề: Table lock block toàn bộ table → giảm concurrency.

-- Xem lock escalation đang xảy ra
SELECT 
    object_name(object_id) AS TableName,
    index_id,
    last_escalation_time,
    escalation_attempt_count,
    escalation_success_count
FROM sys.dm_db_index_operational_stats(DB_ID(), NULL, NULL, NULL)
WHERE escalation_success_count > 0
ORDER BY escalation_success_count DESC;

-- Tắt lock escalation cho table cụ thể
ALTER TABLE dbo.HighTrafficTable SET (LOCK_ESCALATION = DISABLE);

-- Hoặc chỉ cho phép escalate lên partition level (partitioned tables)
ALTER TABLE dbo.PartitionedTable SET (LOCK_ESCALATION = AUTO);

-- Force row-level locks trong query
SELECT * FROM BigTable WITH (ROWLOCK) WHERE Id > 1000;

Monitor Locks hiện tại

-- Xem tất cả locks đang active
SELECT 
    resource_type,
    resource_subtype,
    resource_database_id,
    resource_description,
    resource_associated_entity_id,
    request_mode,
    request_type,
    request_status,
    request_session_id as session_id,
    resource_lock_partition
FROM sys.dm_tran_locks
WHERE resource_database_id = DB_ID()
ORDER BY request_session_id, resource_type;

-- Locks của một session cụ thể
SELECT 
    tl.request_session_id,
    tl.resource_type,
    tl.resource_description,
    tl.request_mode,
    tl.request_status,
    OBJECT_NAME(p.object_id) AS table_name
FROM sys.dm_tran_locks tl
LEFT JOIN sys.partitions p ON tl.resource_associated_entity_id = p.hobt_id
WHERE tl.request_session_id = 55 -- thay session_id
AND tl.resource_database_id = DB_ID();

4. Blocking

Blocking xảy ra như thế nào

Session A (spid 52):
  BEGIN TRAN;
  UPDATE Orders SET Status = 'Processing' WHERE OrderId = 1;
  -- Giữ X lock trên OrderId = 1
  -- Chưa COMMIT...

Session B (spid 67):
  SELECT * FROM Orders WHERE OrderId = 1;
  -- Muốn S lock → CONFLICT với X lock của spid 52
  -- → BLOCKED! Phải chờ spid 52 COMMIT/ROLLBACK

Phát hiện Blocking

-- Cách 1: sp_who2 (đơn giản, nhanh)
EXEC sp_who2;
-- Cột BlkBy: session_id của blocker (0 = không bị block)

-- Cách 2: sys.dm_exec_requests (chi tiết hơn)
SELECT 
    r.session_id,
    r.blocking_session_id,
    r.status,
    r.wait_type,
    r.wait_time / 1000.0 AS wait_seconds,
    r.total_elapsed_time / 1000.0 AS elapsed_seconds,
    DB_NAME(r.database_id) AS database_name,
    SUBSTRING(st.text, (r.statement_start_offset/2)+1,
        ((CASE r.statement_end_offset 
            WHEN -1 THEN DATALENGTH(st.text)
            ELSE r.statement_end_offset 
          END - r.statement_start_offset)/2)+1) AS current_statement,
    qp.query_plan
FROM sys.dm_exec_requests r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) st
CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) qp
WHERE r.blocking_session_id > 0;

-- Cách 3: Cây blocking (blocking chain)
WITH BlockingChain AS (
    SELECT 
        session_id,
        blocking_session_id,
        CAST(session_id AS VARCHAR(100)) AS chain,
        0 AS level
    FROM sys.dm_exec_requests
    WHERE blocking_session_id = 0 AND session_id IN (
        SELECT blocking_session_id FROM sys.dm_exec_requests WHERE blocking_session_id <> 0
    )
    UNION ALL
    SELECT 
        r.session_id,
        r.blocking_session_id,
        bc.chain + ' → ' + CAST(r.session_id AS VARCHAR(10)),
        bc.level + 1
    FROM sys.dm_exec_requests r
    INNER JOIN BlockingChain bc ON r.blocking_session_id = bc.session_id
)
SELECT * FROM BlockingChain ORDER BY chain;

-- Cách 4: sys.dm_os_waiting_tasks
SELECT 
    wt.session_id,
    wt.blocking_session_id,
    wt.wait_type,
    wt.wait_duration_ms / 1000.0 AS wait_seconds,
    wt.resource_description
FROM sys.dm_os_waiting_tasks wt
WHERE wt.blocking_session_id IS NOT NULL;

LOCK_TIMEOUT — Hết kiên nhẫn chờ

-- Set timeout (milliseconds): fail sau 5 giây chờ
SET LOCK_TIMEOUT 5000;

BEGIN TRY
    SELECT * FROM Orders WHERE OrderId = 1; -- Blocked...
    -- Nếu chờ > 5000ms → Error 1222: Lock request time out period exceeded
END TRY
BEGIN CATCH
    IF ERROR_NUMBER() = 1222
        PRINT 'Lock timeout - resource is blocked';
END CATCH;

-- Reset về vô hạn
SET LOCK_TIMEOUT -1;

-- Kiểm tra setting hiện tại
SELECT lock_timeout FROM sys.dm_exec_sessions WHERE session_id = @@SPID;

5. Deadlocks

Deadlock xảy ra như thế nào

Deadlock là tình trạng circular dependency — không ai chịu nhường ai:

Session A (spid 52):              Session B (spid 67):
BEGIN TRAN;                        BEGIN TRAN;
UPDATE TableA SET ...              UPDATE TableB SET ...
  → Giữ X lock trên TableA          → Giữ X lock trên TableB

UPDATE TableB SET ...              UPDATE TableA SET ...
  → Muốn X lock trên TableB          → Muốn X lock trên TableA
  → BLOCKED bởi spid 67             → BLOCKED bởi spid 52

                  ↕ DEADLOCK! ↕
SQL Server chọn một trong hai làm "deadlock victim" và ROLLBACK nó.

Minh hoạ vòng tròn:

spid 52 → chờ spid 67 (lock TableB)
spid 67 → chờ spid 52 (lock TableA)
→ Circular dependency → DEADLOCK

Deadlock Victim Selection

SQL Server chọn victim để ROLLBACK dựa trên:

  1. DEADLOCK_PRIORITY: Session có priority thấp hơn bị chọn
  2. Transaction cost: Session có ít log records hơn (rẻ hơn để rollback) bị chọn
-- Tự nguyện làm victim
SET DEADLOCK_PRIORITY LOW;    -- Range: -10 (LOWEST) đến 10 (HIGHEST), default NORMAL=0

-- Victim nhận lỗi:
-- Msg 1205, Level 13, State 51
-- Transaction (Process ID XX) was deadlocked on lock resources with another process
-- and has been chosen as the deadlock victim. Rerun the transaction.

Phát hiện Deadlock

Cách 1: Trace Flags (Legacy)

-- Bật globally (ảnh hưởng tất cả sessions)
DBCC TRACEON(1222, -1); -- Detailed deadlock info vào Error Log
DBCC TRACEON(1204, -1); -- Basic deadlock info

-- Xem Error Log để thấy deadlock
EXEC sp_readerrorlog 0, 1, 'deadlock';

-- Tắt
DBCC TRACEOFF(1222, -1);

Cách 2: Extended Events (Recommended)

-- Tạo XEvent session để capture deadlock graphs
CREATE EVENT SESSION [DeadlockMonitor] ON SERVER
ADD EVENT sqlserver.xml_deadlock_report(
    ACTION(sqlserver.client_app_name, sqlserver.client_hostname,
           sqlserver.database_name, sqlserver.username)
)
ADD TARGET package0.ring_buffer(SET max_memory = 51200)
WITH (MAX_DISPATCH_LATENCY = 5 SECONDS);

-- Bật session
ALTER EVENT SESSION [DeadlockMonitor] ON SERVER STATE = START;

-- Xem deadlock graphs (XML)
SELECT 
    xdr.value('@timestamp', 'datetime2') AS DeadlockTime,
    xdr.query('.') AS DeadlockGraph
FROM (
    SELECT CAST(target_data AS XML) AS target_data
    FROM sys.dm_xe_session_targets t
    INNER JOIN sys.dm_xe_sessions s ON t.event_session_address = s.address
    WHERE s.name = 'DeadlockMonitor'
    AND t.target_name = 'ring_buffer'
) AS data
CROSS APPLY target_data.nodes('//RingBufferTarget/event') AS XEventData(xdr)
ORDER BY DeadlockTime DESC;

-- Tắt và xóa session khi không cần
ALTER EVENT SESSION [DeadlockMonitor] ON SERVER STATE = STOP;
DROP EVENT SESSION [DeadlockMonitor] ON SERVER;

Cách 3: System Health Session (luôn bật sẵn)

-- SQL Server tự có session 'system_health' luôn capture deadlocks
SELECT 
    xdr.value('@timestamp', 'datetime2') AS DeadlockTime,
    xdr.query('.') AS DeadlockGraph
FROM (
    SELECT CAST(target_data AS XML) AS target_data
    FROM sys.dm_xe_session_targets t
    INNER JOIN sys.dm_xe_sessions s ON t.event_session_address = s.address
    WHERE s.name = 'system_health'
    AND t.target_name = 'ring_buffer'
) AS data
CROSS APPLY target_data.nodes('//RingBufferTarget/event[@name="xml_deadlock_report"]') AS XEventData(xdr)
ORDER BY DeadlockTime DESC;

6. Chiến lược Ngăn chặn Deadlock

Chiến lược 1: Access objects theo cùng thứ tự

-- BAD: T1 lock A rồi B, T2 lock B rồi A → Deadlock tiềm năng
-- Transaction 1:
BEGIN TRAN; UPDATE Accounts ...; UPDATE Inventory ...;

-- Transaction 2:
BEGIN TRAN; UPDATE Inventory ...; UPDATE Accounts ...; -- ← Đảo ngược!

-- GOOD: Cả hai đều lock theo thứ tự A → B
-- Transaction 1:
BEGIN TRAN; UPDATE Accounts ...; UPDATE Inventory ...; COMMIT;

-- Transaction 2:
BEGIN TRAN; UPDATE Accounts ...; UPDATE Inventory ...; COMMIT;

Chiến lược 2: Giữ transaction ngắn

-- BAD: Xử lý nặng trong transaction
BEGIN TRANSACTION;
    SELECT @Data = -- complex calculation
    EXECUTE dbo.SlowProcedure @Data; -- 30 giây
    UPDATE Table1 SET ...;
COMMIT; -- Lock giữ 30+ giây

-- GOOD: Tính toán ngoài transaction
EXECUTE dbo.SlowProcedure @Data; -- Tính ngoài (30 giây, không lock)
BEGIN TRANSACTION;
    UPDATE Table1 SET ...;
COMMIT; -- Lock chỉ giữ mili giây

Chiến lược 3: Dùng UPDLOCK thay vì S → X conversion

-- BAD: Deadlock pattern phổ biến (Select then Update)
BEGIN TRAN;
    SELECT @Stock = Stock FROM Products WHERE ProductId = 1; -- S lock
    -- Thời điểm này: Session khác cũng lấy S lock!
    UPDATE Products SET Stock = @Stock - 1 WHERE ProductId = 1;
    -- Cả hai cùng muốn upgrade S → X → DEADLOCK!

-- GOOD: Lấy U lock từ đầu
BEGIN TRAN;
    SELECT @Stock = Stock 
    FROM Products WITH (UPDLOCK) -- U lock ngay từ đầu
    WHERE ProductId = 1;
    -- Session khác muốn UPDLOCK sẽ bị block (không deadlock!)
    UPDATE Products SET Stock = @Stock - 1 WHERE ProductId = 1;
COMMIT;

Chiến lược 4: Thêm index phù hợp

-- Thiếu index → Table/Index scan → Lock nhiều rows hơn cần thiết → Tăng deadlock risk
-- Với index tốt → Seek → Chỉ lock rows cần thiết

-- Kiểm tra missing indexes
SELECT 
    mid.statement AS missing_index_table,
    mid.equality_columns,
    mid.inequality_columns,
    mid.included_columns,
    migs.avg_user_impact AS estimated_improvement_pct,
    migs.user_seeks * migs.avg_total_user_cost * (migs.avg_user_impact / 100.0) AS score
FROM sys.dm_db_missing_index_details mid
INNER JOIN sys.dm_db_missing_index_groups mig ON mid.index_handle = mig.index_handle
INNER JOIN sys.dm_db_missing_index_group_stats migs ON mig.index_group_handle = migs.group_handle
ORDER BY score DESC;

Chiến lược 5: Dùng RCSI/Snapshot để loại bỏ Read-Write Deadlocks

-- Bật RCSI: Readers không lấy S locks → Read-Write deadlock không thể xảy ra
ALTER DATABASE MyDatabase SET READ_COMMITTED_SNAPSHOT ON;
-- 70-80% deadlocks là Read-Write → RCSI loại bỏ phần lớn deadlocks

Chiến lược 6: Retry Logic trong Application

// C# với Polly
var policy = Policy
    .Handle<SqlException>(ex => ex.Number == 1205) // Deadlock victim
    .WaitAndRetry(
        retryCount: 3,
        sleepDurationProvider: attempt => 
            TimeSpan.FromMilliseconds(100 * Math.Pow(2, attempt)) // Exponential backoff
            + TimeSpan.FromMilliseconds(Random.Shared.Next(0, 100)) // Jitter
    );

await policy.ExecuteAsync(async () => {
    await ExecuteTransactionAsync();
});

7. Optimistic vs Pessimistic Concurrency

Pessimistic Concurrency (Locking-based)

Giả định: Conflicts xảy ra thường xuyên → Phải lock trước
Cơ chế: Lock tài nguyên trước khi đọc/ghi, giữ lock cho đến khi xong
Trade-off: Consistency cao, Concurrency thấp
Phù hợp: Môi trường high-contention, tài chính
-- Pessimistic: Lock ngay khi đọc
BEGIN TRAN;
    SELECT * FROM Products WITH (UPDLOCK, ROWLOCK) WHERE ProductId = 1;
    -- Bây giờ không ai có thể update ProductId = 1
    UPDATE Products SET Stock = Stock - 1 WHERE ProductId = 1;
COMMIT;

Optimistic Concurrency (Versioning-based)

Giả định: Conflicts hiếm khi xảy ra → Không cần lock
Cơ chế: Đọc không lock, khi ghi thì kiểm tra xem data có thay đổi không
Trade-off: Concurrency cao, phải retry khi có conflict
Phù hợp: Môi trường low-contention, web APIs
-- Optimistic với rowversion/timestamp column:
-- 1. Đọc với rowversion
SELECT ProductId, Stock, rowver FROM Products WHERE ProductId = 1;
-- → Nhận được: ProductId=1, Stock=100, rowver=0x0000000000000123

-- 2. Ghi với điều kiện rowver không đổi
UPDATE Products 
SET Stock = Stock - 1
WHERE ProductId = 1 AND rowver = 0x0000000000000123; -- Optimistic check!

IF @@ROWCOUNT = 0
    THROW 50001, 'Concurrent update detected - please retry', 1;
    -- Conflict! Row đã bị modify bởi người khác

8. Hints: NOLOCK, UPDLOCK, HOLDLOCK, ROWLOCK

NOLOCK (READUNCOMMITTED)

-- ⚠️ NGUY HIỂM: Dirty Read, skip rows, duplicate rows
SELECT COUNT(*) FROM Orders WITH (NOLOCK); -- Approximate count

-- KHÔNG BAO GIỜ dùng NOLOCK khi:
-- ✗ Tính toán tài chính (SUM, COUNT cho báo cáo chính xác)
-- ✗ Business logic dựa trên kết quả
-- ✗ Foreign key lookups
-- ✗ Kiểm tra tồn kho trước khi bán

UPDLOCK

-- Lấy U lock thay vì S lock — ngăn deadlock khi scan rồi update
SELECT ProductId, Stock 
FROM Products WITH (UPDLOCK)
WHERE Category = 'Electronics' AND Stock < 10;
-- Các rows này không thể bị concurrent update vì U lock

HOLDLOCK (= SERIALIZABLE)

-- Giữ S lock cho đến khi transaction kết thúc (như SERIALIZABLE)
SELECT * FROM Orders WITH (HOLDLOCK) WHERE CustomerId = 1;
-- Không ai có thể INSERT rows mới thỏa WHERE condition trong khi transaction đang chạy

-- Kết hợp
SELECT * FROM Orders WITH (UPDLOCK, HOLDLOCK) WHERE CustomerId = 1;
-- Vừa U lock, vừa giữ range lock

ROWLOCK

-- Force SQL Server dùng row-level lock (chống lock escalation)
UPDATE Orders WITH (ROWLOCK) 
SET Status = 'Processed' 
WHERE OrderDate < DATEADD(DAY, -1, GETDATE());

PAGLOCK, TABLOCK, TABLOCKX

-- PAGLOCK: Dùng page-level lock
-- TABLOCK: Lock toàn bộ table với S lock
-- TABLOCKX: Lock toàn bộ table với X lock (exclusive)

-- TABLOCK thường dùng với bulk operations
BULK INSERT dbo.StagingTable 
FROM 'C:\data\import.csv'
WITH (TABLOCK); -- Cho phép parallel bulk insert, minimal logging

9. sys.dm_tran_locks — Xem Locks hiện tại

-- Tất cả locks trong database hiện tại
SELECT
    tl.request_session_id AS SPID,
    tl.resource_type,
    tl.resource_subtype,
    CASE tl.resource_type
        WHEN 'OBJECT' THEN OBJECT_NAME(tl.resource_associated_entity_id)
        WHEN 'DATABASE' THEN DB_NAME(tl.resource_database_id)
        ELSE tl.resource_description
    END AS resource_name,
    tl.request_mode AS lock_mode,
    tl.request_status AS lock_status,
    tl.request_type,
    es.login_name,
    es.host_name,
    es.program_name
FROM sys.dm_tran_locks tl
JOIN sys.dm_exec_sessions es ON tl.request_session_id = es.session_id
WHERE tl.resource_database_id = DB_ID()
AND tl.resource_type <> 'DATABASE'
ORDER BY tl.request_session_id, tl.resource_type;

-- Tìm locks đang conflict (WAIT status)
SELECT
    blocked.request_session_id AS blocked_spid,
    blocker.request_session_id AS blocking_spid,
    blocked.resource_type,
    OBJECT_NAME(blocked.resource_associated_entity_id) AS object_name,
    blocked.request_mode AS blocked_lock_mode,
    blocker.request_mode AS blocking_lock_mode
FROM sys.dm_tran_locks blocked
JOIN sys.dm_tran_locks blocker 
    ON blocked.resource_associated_entity_id = blocker.resource_associated_entity_id
    AND blocked.resource_type = blocker.resource_type
WHERE blocked.request_status = 'WAIT'
AND blocker.request_status = 'GRANT';

10. Script tổng hợp: Blocking & Deadlock Diagnostic

-- =====================================================
-- Complete Blocking & Deadlock Diagnostic Script
-- =====================================================

-- 1. Tổng quan blocking ngay lập tức
PRINT '=== BLOCKING OVERVIEW ===';
SELECT
    r.session_id AS blocked_spid,
    r.blocking_session_id AS blocker_spid,
    r.wait_type,
    r.wait_time / 1000.0 AS wait_seconds,
    s.login_name AS blocked_login,
    s.host_name AS blocked_host,
    SUBSTRING(st.text, (r.statement_start_offset/2)+1, 200) AS blocked_query,
    s2.login_name AS blocker_login,
    s2.host_name AS blocker_host
FROM sys.dm_exec_requests r
JOIN sys.dm_exec_sessions s ON r.session_id = s.session_id
JOIN sys.dm_exec_sessions s2 ON r.blocking_session_id = s2.session_id
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) st
WHERE r.blocking_session_id > 0;

-- 2. Open transactions (tất cả)
PRINT '=== OPEN TRANSACTIONS ===';
SELECT
    s.session_id,
    s.login_name,
    s.host_name,
    s.open_transaction_count,
    DATEDIFF(SECOND, at.transaction_begin_time, GETDATE()) AS tran_seconds_open,
    at.name AS transaction_name,
    SUBSTRING(st.text, 1, 200) AS last_sql
FROM sys.dm_exec_sessions s
JOIN sys.dm_tran_session_transactions tst ON s.session_id = tst.session_id
JOIN sys.dm_tran_active_transactions at ON tst.transaction_id = at.transaction_id
LEFT JOIN sys.dm_exec_requests r ON s.session_id = r.session_id
OUTER APPLY sys.dm_exec_sql_text(r.sql_handle) st
WHERE s.open_transaction_count > 0
ORDER BY tran_seconds_open DESC;

-- 3. Lock summary
PRINT '=== LOCK SUMMARY BY TABLE ===';
SELECT
    OBJECT_NAME(p.object_id) AS table_name,
    tl.resource_type,
    tl.request_mode,
    COUNT(*) AS lock_count
FROM sys.dm_tran_locks tl
JOIN sys.partitions p ON tl.resource_associated_entity_id = p.hobt_id
WHERE tl.resource_database_id = DB_ID()
AND tl.resource_type IN ('KEY', 'PAGE', 'RID')
GROUP BY OBJECT_NAME(p.object_id), tl.resource_type, tl.request_mode
ORDER BY lock_count DESC;

Best Practices Tóm tắt

Vấn đềGiải pháp
Blocking phổ biếnBật RCSI, tối ưu index, rút ngắn transactions
DeadlockConsistent lock order, UPDLOCK, RCSI, retry logic
Lock EscalationLOCK_ESCALATION = DISABLE, ROWLOCK hint, nhỏ batches
NOLOCK bừa bãiDùng RCSI hoặc SNAPSHOT thay thế
Long-running transactionsBatch processing, monitor @@TRANCOUNT
Không biết ai đang blocksp_who2, sys.dm_exec_requests, Extended Events

Security & Administration - Tổng quan

Phần này bao gồm các kiến thức về Bảo mật (Security) và Quản trị (Administration) SQL Server — những chủ đề quan trọng không chỉ cho DBA mà còn cho Developer khi thiết kế ứng dụng an toàn.

Các chủ đề con

Chủ đềMô tả
Bảo mật SQL ServerAuthentication, Permissions, RLS, TDE, Always Encrypted, Audit, SQL Injection
Backup & RecoveryFull/Differential/Log Backup, Recovery Models, RESTORE, Point-in-Time Recovery
SQL Server Agent & JobsScheduling, Jobs, Alerts, Operators
Monitoring & DiagnosticsDMVs, Performance counters, Wait stats, Query Store

Q&A Phỏng vấn


🟢 Junior Level

Q1: Hai chế độ Authentication trong SQL Server là gì?

A:

  • Windows Authentication (Integrated Security): Xác thực qua Active Directory. SQL Server tin tưởng Windows đã xác thực user. Bảo mật hơn vì không cần password trong connection string. Là chế độ khuyến nghị.
  • SQL Server Authentication: User/password được lưu trong SQL Server, không phụ thuộc vào Windows. Cần thiết khi connect từ non-Windows systems (Linux, Mac, containers).
  • Mixed Mode: Hỗ trợ cả hai. Nên tắt SQL Server Authentication nếu không cần thiết (nguyên tắc least privilege).

Q2: Sự khác biệt giữa Login và User trong SQL Server là gì?

A:

  • Login: Tồn tại ở server level — đây là authentication principal, cho phép kết nối vào SQL Server instance.
  • User: Tồn tại ở database level — là authorization principal trong database cụ thể, mapping đến Login.
Login [DOMAIN\HuyNgo]  ←→  User [HuyNgo] trong database SalesDB
(Server level)               (Database level)

Một Login có thể map đến User trong nhiều databases. Nếu không có User mapping, Login không thể truy cập database.


Q3: GRANT, DENY, REVOKE khác nhau như thế nào?

A:

  • GRANT: Cấp quyền cho principal.
  • REVOKE: Xóa bỏ quyền đã GRANT hoặc DENY (về trạng thái “không có quyền”).
  • DENY: Từ chối tường minh quyền — DENY luôn thắng GRANT dù user được GRANT qua role.
GRANT SELECT ON dbo.Products TO UserA;  -- UserA có thể SELECT
DENY SELECT ON dbo.Orders TO UserA;     -- UserA KHÔNG thể SELECT Orders dù qua role nào
REVOKE SELECT ON dbo.Products TO UserA; -- UserA không còn quyền SELECT Products được GRANT trực tiếp

Q4: SQL Injection là gì và cách phòng chống?

A: SQL Injection là tấn công khi attacker chèn SQL code độc hại vào input để thay đổi query logic.

-- VULNERABLE: String concatenation
DECLARE @sql NVARCHAR(500) = 'SELECT * FROM Users WHERE Name = ''' + @input + '''';
-- Input: ' OR '1'='1 → Lấy được tất cả users!

-- SAFE: Parameterized query
DECLARE @sql NVARCHAR(500) = 'SELECT * FROM Users WHERE Name = @name';
EXEC sp_executesql @sql, N'@name NVARCHAR(100)', @name = @input;

-- SAFE: Stored procedure với parameters
CREATE PROCEDURE dbo.GetUser @Name NVARCHAR(100)
AS SELECT * FROM Users WHERE Name = @Name;
-- Parameter không được interpret là SQL code

Q5: Các Fixed Database Roles phổ biến là gì?

A:

RoleQuyền hạn
db_ownerFull control database
db_datareaderSELECT trên tất cả tables
db_datawriterINSERT/UPDATE/DELETE trên tất cả tables
db_ddladminTạo/sửa schema objects
db_securityadminQuản lý permissions, roles
db_backupoperatorBackup database
publicMọi user đều thuộc role này

Q6: Làm thế nào để tạo Login và User cho một ứng dụng?

A:

-- Server level: Tạo Login
CREATE LOGIN AppLogin WITH PASSWORD = 'Str0ng!P@ssw0rd';

-- Database level: Tạo User và map với Login
USE MyDatabase;
CREATE USER AppUser FOR LOGIN AppLogin;

-- Gán Role
ALTER ROLE db_datareader ADD MEMBER AppUser;
ALTER ROLE db_datawriter ADD MEMBER AppUser;

-- Hoặc GRANT cụ thể
GRANT EXECUTE ON SCHEMA::dbo TO AppUser;

Q7: Transparent Data Encryption (TDE) là gì?

A: TDE mã hóa toàn bộ database files (data file .mdf, log file .ldf, backup files) ở tầng storage — “encryption at rest”. Dữ liệu được decrypt tự động khi đọc vào memory, không cần thay đổi application code.

Bảo vệ chống: Kẻ tấn công lấy file .mdf trực tiếp từ disk sẽ không đọc được.
Không bảo vệ: Ai có quyền query database vẫn đọc được dữ liệu bình thường.


Q8: Backup types trong SQL Server là gì?

A:

Backup TypeNội dungRecovery Model
FullToàn bộ databaseTất cả
DifferentialThay đổi từ lần Full backup cuốiTất cả
Transaction LogLog records từ lần log backup cuốiFull, Bulk-logged
File/FilegroupBackup từng fileFull, Bulk-logged
Copy-onlyFull backup không ảnh hưởng backup chainTất cả

Q9: Recovery Models trong SQL Server là gì?

A:

ModelLog truncationPoint-in-time Recovery
SIMPLEKhi checkpoint❌ Không hỗ trợ
FULLKhi log backup✅ Hỗ trợ
BULK_LOGGEDKhi log backup✅ Hỗ trợ (giới hạn khi có bulk-logged ops)

Q10: sp_who2 và Activity Monitor dùng để làm gì?

A:

  • sp_who2: Stored procedure hiển thị tất cả active sessions, blocking info, CPU/Disk usage. Dùng để diagnose nhanh.
  • Activity Monitor: GUI trong SSMS hiển thị processes, waits, expensive queries, data file I/O.
EXEC sp_who2; -- Xem tất cả sessions
EXEC sp_who2 'active'; -- Chỉ active sessions
EXEC sp_who2 55; -- Session cụ thể

🟡 Mid Level

Q11: Row-Level Security (RLS) là gì và dùng để làm gì?

A: RLS cho phép kiểm soát quyền truy cập ở cấp row dựa trên người dùng đang query. Một table có thể trả về các rows khác nhau cho các user khác nhau — hoàn toàn transparent với application.

-- Ví dụ: Mỗi sales rep chỉ thấy orders của mình
CREATE FUNCTION dbo.fn_SecurityPredicate(@SalesRepId INT)
RETURNS TABLE
WITH SCHEMABINDING
AS RETURN
    SELECT 1 AS result
    WHERE @SalesRepId = CAST(SESSION_CONTEXT(N'SalesRepId') AS INT)
        OR IS_MEMBER('db_owner') = 1; -- Managers thấy tất cả

CREATE SECURITY POLICY SalesRepPolicy
ADD FILTER PREDICATE dbo.fn_SecurityPredicate(SalesRepId) ON dbo.Orders,
ADD BLOCK PREDICATE dbo.fn_SecurityPredicate(SalesRepId) ON dbo.Orders
WITH (STATE = ON);

-- Application set context trước khi query:
EXEC sys.sp_set_session_context @key = N'SalesRepId', @value = 42;
SELECT * FROM Orders; -- Tự động filter chỉ orders của SalesRepId = 42

Q12: Dynamic Data Masking (DDM) là gì?

A: DDM che giấu dữ liệu nhạy cảm cho các user không có quyền xem — không thay đổi dữ liệu thực, chỉ thay đổi cách hiển thị.

CREATE TABLE Customers (
    CustomerId INT PRIMARY KEY,
    FullName NVARCHAR(100) MASKED WITH (FUNCTION = 'partial(2,"...",2)'),      -- "Hu...go"
    Email NVARCHAR(200) MASKED WITH (FUNCTION = 'email()'),                    -- "hXX@XXXX.com"
    Phone NVARCHAR(20) MASKED WITH (FUNCTION = 'partial(0,"XXX-XXX-",4)'),   -- "XXX-XXX-1234"
    CreditCard NVARCHAR(20) MASKED WITH (FUNCTION = 'partial(0,"XXXX-XXXX-XXXX-",4)'), -- "XXXX-XXXX-XXXX-5678"
    Salary DECIMAL(10,2) MASKED WITH (FUNCTION = 'random(1, 100)')            -- Random số
);

-- User thường thấy:
-- FullName: "Hu...go", Email: "hXX@XXXX.com"

-- User có quyền UNMASK thấy dữ liệu thật:
GRANT UNMASK ON dbo.Customers TO PowerUser;
-- Hoặc UNMASK toàn database:
GRANT UNMASK TO PowerUser;

Q13: EXECUTE AS là gì và khi nào dùng?

A: EXECUTE AS cho phép thay đổi security context khi thực thi code — impersonate một user/login khác.

-- Stored procedure chạy dưới context của owner (không phải caller)
CREATE PROCEDURE dbo.GetSensitiveData
WITH EXECUTE AS OWNER -- hoặc EXECUTE AS 'SpecificUser'
AS
BEGIN
    -- Thực thi với quyền của OWNER dù caller không có quyền
    SELECT * FROM dbo.SensitiveTable;
END;

-- Gọi procedure:
-- User A không có SELECT trên SensitiveTable
-- Nhưng User A có EXECUTE quyền trên procedure
-- → Cho phép! (Ownership chaining)

-- Impersonation tạm thời:
EXECUTE AS USER = 'LimitedUser';
SELECT * FROM dbo.Orders; -- Thực thi dưới quyền LimitedUser
REVERT; -- Quay lại context gốc

Q14: Always Encrypted là gì? Khác TDE như thế nào?

A:

TDEAlways Encrypted
LoạiEncryption at restClient-side encryption
DBA có đọc được?✅ Có❌ Không
Application cần thay đổi?❌ Không✅ Có (driver config)
Key quản lý bởi ai?SQL ServerClient/Application
Bảo vệ khỏiDisk theftDBA, Cloud admin, SQL injection
-- Always Encrypted: Dữ liệu được mã hóa TRƯỚC khi tới SQL Server
-- SQL Server chỉ thấy ciphertext, không thể đọc plaintext

-- Cột được tạo với encryption:
CREATE TABLE Patients (
    PatientId INT PRIMARY KEY,
    Name NVARCHAR(100),
    SSN NVARCHAR(11) ENCRYPTED WITH (
        COLUMN_ENCRYPTION_KEY = SSN_CEK,
        ENCRYPTION_TYPE = Randomized, -- hoặc Deterministic
        ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256'
    )
);
-- DBA query SELECT SSN FROM Patients → thấy ciphertext, không đọc được
-- Application (có CMK) → thấy plaintext

Q15: SQL Server Audit là gì? Thiết lập như thế nào?

A: SQL Server Audit cho phép track và log các hành động security/database vào file, Windows Event Log, hoặc Application Log.

-- Tạo Server Audit
CREATE SERVER AUDIT SecurityAudit
TO FILE (FILEPATH = 'C:\SQLAudit\', MAXSIZE = 100 MB, MAX_FILES = 10)
WITH (ON_FAILURE = CONTINUE);

ALTER SERVER AUDIT SecurityAudit WITH (STATE = ON);

-- Audit đăng nhập thất bại
CREATE SERVER AUDIT SPECIFICATION LoginAuditSpec
FOR SERVER AUDIT SecurityAudit
ADD (FAILED_LOGIN_GROUP),
ADD (SUCCESSFUL_LOGIN_GROUP);
ALTER SERVER AUDIT SPECIFICATION LoginAuditSpec WITH (STATE = ON);

-- Database audit: Track DML trên table nhạy cảm
CREATE DATABASE AUDIT SPECIFICATION SensitiveDataAudit
FOR SERVER AUDIT SecurityAudit
ADD (SELECT, INSERT, UPDATE, DELETE ON dbo.Customers BY public)
WITH (STATE = ON);

-- Xem audit log
SELECT 
    event_time,
    action_id,
    succeeded,
    session_server_principal_name AS login_name,
    statement,
    server_instance_name
FROM sys.fn_get_audit_file('C:\SQLAudit\*.sqlaudit', DEFAULT, DEFAULT)
ORDER BY event_time DESC;

Q16: Schema-based permissions là gì? Lợi ích?

A: Thay vì GRANT quyền trên từng object, có thể GRANT trên toàn bộ schema — tất cả objects trong schema tự động inherit permission.

-- Tạo schema riêng cho app
CREATE SCHEMA app AUTHORIZATION dbo;

-- Tạo objects trong schema
CREATE TABLE app.Orders (...);
CREATE TABLE app.Products (...);
CREATE PROCEDURE app.GetOrders AS ...;

-- GRANT một lần cho toàn schema (thay vì từng object)
GRANT SELECT, INSERT, UPDATE, DELETE ON SCHEMA::app TO AppUser;
GRANT EXECUTE ON SCHEMA::app TO AppUser;
-- AppUser tự động có quyền trên mọi object trong schema app

-- Optional: Tạo schema riêng cho reporting (read-only)
CREATE SCHEMA rpt AUTHORIZATION dbo;
GRANT SELECT ON SCHEMA::rpt TO ReportUser;

Q17: Backup Strategy tốt nhất là gì?

A: 3-2-1 Rule cho database backup:

  • 3 copies của data
  • 2 different storage types
  • 1 offsite/cloud copy

Lịch backup thực tế:

-- Daily Full Backup (11 PM)
BACKUP DATABASE SalesDB 
TO DISK = 'D:\Backups\SalesDB_Full_' + CONVERT(VARCHAR,GETDATE(),112) + '.bak'
WITH COMPRESSION, CHECKSUM, STATS = 10;

-- Every 6 hours: Differential Backup
BACKUP DATABASE SalesDB 
TO DISK = 'D:\Backups\SalesDB_Diff_' + CONVERT(VARCHAR,GETDATE(),112) + '_' + ...
WITH DIFFERENTIAL, COMPRESSION;

-- Every 15-30 minutes: Transaction Log Backup (Recovery Model = FULL)
BACKUP LOG SalesDB 
TO DISK = 'D:\Backups\SalesDB_Log_' + ... + '.trn'
WITH COMPRESSION;

-- Test restore regularly!
RESTORE DATABASE SalesDB_Test 
FROM DISK = '...' 
WITH NORECOVERY, MOVE ..., MOVE ...;
RESTORE LOG SalesDB_Test FROM DISK = '...' WITH RECOVERY;
DBCC CHECKDB(SalesDB_Test); -- Verify integrity

Q18: Làm thế nào để giám sát SQL Server Agent Jobs?

A:

-- Xem tất cả jobs và trạng thái
SELECT 
    j.name AS job_name,
    j.enabled,
    h.run_date,
    h.run_time,
    CASE h.run_status
        WHEN 0 THEN 'Failed'
        WHEN 1 THEN 'Succeeded'
        WHEN 2 THEN 'Retry'
        WHEN 3 THEN 'Cancelled'
    END AS last_run_status,
    h.run_duration,
    h.message
FROM msdb.dbo.sysjobs j
LEFT JOIN msdb.dbo.sysjobhistory h ON j.job_id = h.job_id
    AND h.instance_id = (
        SELECT MAX(instance_id) FROM msdb.dbo.sysjobhistory
        WHERE job_id = j.job_id AND step_id = 0
    )
ORDER BY j.name;

-- Xem jobs đang chạy hiện tại
EXEC msdb.dbo.sp_help_job @execution_status = 1; -- 1 = Running

Q19: Wait Statistics là gì? Dùng để làm gì?

A: Wait Statistics cho biết SQL Server đang “chờ” gì nhiều nhất — là điểm khởi đầu để tìm performance bottleneck.

-- Top wait types (kể từ khi SQL Server restart)
SELECT TOP 20
    wait_type,
    waiting_tasks_count,
    wait_time_ms / 1000.0 AS wait_time_seconds,
    max_wait_time_ms / 1000.0 AS max_wait_seconds,
    (wait_time_ms - signal_wait_time_ms) / 1000.0 AS resource_wait_seconds
FROM sys.dm_os_wait_stats
WHERE wait_type NOT IN (
    -- Lọc bỏ background waits không liên quan
    'SLEEP_TEMPDBSTARTUP', 'SLEEP_DBSTARTUP', 'LAZYWRITER_SLEEP',
    'LOGMGR_QUEUE', 'CHECKPOINT_QUEUE', 'REQUEST_FOR_DEADLOCK_SEARCH',
    'RESOURCE_QUEUE', 'SERVER_IDLE_CHECK', 'SLEEP_TASK',
    'SLEEP_SYSTEMTASK', 'SLEEP_TEMPDBSTARTUP', 'SNI_HTTP_ACCEPT',
    'SP_SERVER_DIAGNOSTICS_SLEEP', 'SQLTRACE_BUFFER_FLUSH', 'WAITFOR',
    'BROKER_TO_FLUSH', 'BROKER_TASK_STOP', 'CLR_AUTO_EVENT', 'DISPATCHER_QUEUE_SEMAPHORE',
    'FT_IFTS_SCHEDULER_IDLE_WAIT', 'XE_DISPATCHER_WAIT', 'XE_TIMER_EVENT'
)
ORDER BY wait_time_ms DESC;

Giải thích common wait types:

Wait TypeNghĩa
LCK_M_*Lock wait (blocking)
PAGEIOLATCH_*Đọc page từ disk (I/O bottleneck)
CXPACKETParallel query coordination
SOS_SCHEDULER_YIELDCPU pressure
ASYNC_NETWORK_IOClient không đọc kết quả đủ nhanh
WRITELOGLog flush (commit overhead)

Q20: Query Store là gì?

A: Query Store là tính năng built-in (SQL Server 2016+) tự động capture query plans và runtime stats. Giúp:

  • Phát hiện plan regression (query đột ngột chậm vì plan thay đổi)
  • Force execution plan cũ nếu plan mới tệ hơn
  • Analyze top expensive queries
-- Bật Query Store
ALTER DATABASE MyDB SET QUERY_STORE = ON;
ALTER DATABASE MyDB SET QUERY_STORE (
    OPERATION_MODE = READ_WRITE,
    CLEANUP_POLICY = (STALE_QUERY_THRESHOLD_DAYS = 30),
    DATA_FLUSH_INTERVAL_SECONDS = 900,
    MAX_STORAGE_SIZE_MB = 1024
);

-- Top queries theo CPU
SELECT TOP 10
    qsq.query_id,
    qsq.query_hash,
    SUM(qsrs.avg_cpu_time) AS total_avg_cpu,
    SUM(qsrs.avg_duration) AS total_avg_duration,
    SUM(qsrs.count_executions) AS total_executions,
    SUBSTRING(qsqt.query_sql_text, 1, 200) AS query_text
FROM sys.query_store_query qsq
JOIN sys.query_store_query_text qsqt ON qsq.query_text_id = qsqt.query_text_id
JOIN sys.query_store_plan qsp ON qsq.query_id = qsp.query_id
JOIN sys.query_store_runtime_stats qsrs ON qsp.plan_id = qsrs.plan_id
GROUP BY qsq.query_id, qsq.query_hash, qsqt.query_sql_text
ORDER BY total_avg_cpu DESC;

-- Force một plan cụ thể
EXEC sys.sp_query_store_force_plan @query_id = 42, @plan_id = 5;

🔴 Senior Level

Q21: Thiết kế Permission Model cho một ứng dụng multi-tenant như thế nào?

A: Có nhiều cách tiếp cận:

Approach 1: Schema-based separation

-- Mỗi tenant có schema riêng
CREATE SCHEMA Tenant1 AUTHORIZATION dbo;
CREATE SCHEMA Tenant2 AUTHORIZATION dbo;

-- App login per tenant
CREATE LOGIN Tenant1Login WITH PASSWORD = '...';
CREATE USER Tenant1User FOR LOGIN Tenant1Login;
GRANT SELECT, INSERT, UPDATE, DELETE ON SCHEMA::Tenant1 TO Tenant1User;
DENY SELECT ON SCHEMA::Tenant2 TO Tenant1User; -- Tường minh deny cross-tenant

-- RLS thêm cho Defense in depth

Approach 2: RLS với TenantId column

-- Tất cả dữ liệu trong cùng schema, RLS filter theo tenant
ALTER TABLE Orders ADD TenantId INT NOT NULL;

CREATE FUNCTION dbo.fn_TenantFilter(@TenantId INT)
RETURNS TABLE WITH SCHEMABINDING AS RETURN
    SELECT 1 AS OK
    WHERE @TenantId = CONVERT(INT, SESSION_CONTEXT(N'TenantId'));

CREATE SECURITY POLICY TenantPolicy
ADD FILTER PREDICATE dbo.fn_TenantFilter(TenantId) ON dbo.Orders,
ADD BLOCK PREDICATE dbo.fn_TenantFilter(TenantId) ON dbo.Orders
WITH (STATE = ON);

Q22: Giải thích Cross-database Ownership Chaining.

A: Khi một stored procedure trong DB_A query sang DB_B, thông thường SQL Server kiểm tra permissions user trên DB_B objects. Với ownership chaining, nếu cùng owner (thường là dbo), SQL Server bỏ qua permission check trên DB_B.

-- DB_A.dbo.GetData procedure:
CREATE PROCEDURE DB_A.dbo.GetCrossDBData
AS
    SELECT * FROM DB_B.dbo.SensitiveTable; -- Cross-DB reference

-- Nếu DB_A.dbo và DB_B.dbo cùng owner:
-- User chỉ cần EXECUTE quyền trên procedure, không cần SELECT trên DB_B.dbo.SensitiveTable

-- Bật/tắt Cross-database ownership chaining:
ALTER DATABASE DB_B SET DB_CHAINING ON;  -- Cho phép
ALTER DATABASE DB_B SET DB_CHAINING OFF; -- Tắt (secure)

-- Server level:
sp_configure 'cross db ownership chaining', 1; -- Bật cho tất cả DB
RECONFIGURE;

Rủi ro: Có thể vô tình mở quyền truy cập cross-database không mong muốn. Best practice: Tắt và dùng explicit permissions hoặc EXECUTE AS.


Q23: Làm thế nào để điều tra và ngăn chặn SQL Injection trong stored procedures?

A:

Phát hiện:

-- Tìm dynamic SQL không dùng sp_executesql (risky patterns)
SELECT 
    OBJECT_NAME(object_id) AS object_name,
    OBJECT_SCHEMA_NAME(object_id) AS schema_name,
    definition
FROM sys.sql_modules
WHERE definition LIKE '%EXEC(%+%'  -- EXEC với string concatenation
   OR definition LIKE '%EXECUTE(%+%'
   OR (definition LIKE '%@%+%' AND definition LIKE '%EXEC%')
ORDER BY object_name;

Phòng chống:

-- BAD: 
DECLARE @sql VARCHAR(500) = 'SELECT * FROM ' + @TableName;
EXEC(@sql); -- Table name injection!

-- GOOD: Whitelist validation
IF @TableName NOT IN ('Orders', 'Products', 'Customers')
    THROW 50001, 'Invalid table name', 1;
DECLARE @sql NVARCHAR(500) = N'SELECT * FROM ' + QUOTENAME(@TableName);
EXEC sp_executesql @sql;

-- QUOTENAME: Wrap identifier in quotes, escape special chars
SELECT QUOTENAME('Orders; DROP TABLE Users--');
-- Trả về: [Orders; DROP TABLE Users--] → An toàn!

-- BEST: Tránh dynamic SQL hoàn toàn nếu có thể

Q24: Thiết kế Backup & Recovery Strategy cho production database 99.99% uptime?

A:

Recovery Point Objective (RPO): Mất tối đa bao nhiêu data? (e.g., 5 phút)
Recovery Time Objective (RTO): Restore trong bao lâu? (e.g., 1 giờ)

Strategy cho RPO=5min, RTO=1h:

-- 1. Cấu hình
ALTER DATABASE CriticalDB SET RECOVERY FULL;

-- 2. Lịch backup
-- Weekly: Full backup (Sunday 2AM)
-- Daily: Differential backup (Mon-Sat 2AM)  
-- Every 5 minutes: Transaction Log backup

-- 3. Always On Availability Groups (HADR)
-- Primary: Xử lý writes
-- Secondary (synchronous): Auto-failover trong vài giây (RTO << 1h)
-- Secondary (async, different DC): DR copy (địa lý khác nhau)

-- 4. Monitor backup
SELECT 
    bs.database_name,
    bs.type AS backup_type, -- D=Full, I=Differential, L=Log
    bs.backup_finish_date,
    DATEDIFF(MINUTE, bs.backup_start_date, bs.backup_finish_date) AS duration_minutes,
    bs.backup_size / 1024 / 1024 AS size_MB
FROM msdb.dbo.backupset bs
WHERE bs.database_name = 'CriticalDB'
ORDER BY bs.backup_finish_date DESC;

-- 5. Test restore quarterly (drills!)
-- Point-in-Time Recovery test:
RESTORE DATABASE CriticalDB_Test FROM DISK = '...' 
WITH NORECOVERY, REPLACE, MOVE ... ;
RESTORE LOG CriticalDB_Test FROM DISK = '...' 
WITH RECOVERY, STOPAT = '2026-04-01 15:30:00'; -- Restore đến thời điểm cụ thể

Q25: Giải thích Always On Availability Groups và cách nó cải thiện bảo mật/availability.

A:

Always On AG Architecture:
┌─────────────────────────────────────────────────────────┐
│  Primary Replica (DC1)          Secondary Replicas       │
│  ┌──────────────────┐    Sync   ┌──────────────────┐   │
│  │ SQL Server A     │ ─────────► │ SQL Server B     │   │
│  │ (Read-Write)     │           │ (Auto-Failover)   │   │
│  └──────────────────┘    Async  └──────────────────┘   │
│                          ──────► SQL Server C (DR site)  │
└─────────────────────────────────────────────────────────┘
                │
         Listener VIP (Virtual IP)
                │
     Application connect to Listener
     → Automatically routes to Primary

Bảo mật với AG:

-- Secondary readable replicas cho reporting (không load primary)
-- Configure readable secondary:
ALTER AVAILABILITY GROUP MyAG
MODIFY REPLICA ON 'SQL-B' 
WITH (SECONDARY_ROLE (ALLOW_CONNECTIONS = READ_ONLY));

-- Backup từ secondary (giảm tải primary)
ALTER AVAILABILITY GROUP MyAG
MODIFY REPLICA ON 'SQL-B'
WITH (BACKUP_PRIORITY = 90); -- Ưu tiên backup từ secondary

Bảo mật SQL Server

Bảo mật SQL Server là lớp phòng thủ quan trọng trong kiến trúc ứng dụng. Một database không được bảo mật đúng cách có thể gây rò rỉ dữ liệu, vi phạm compliance, và thiệt hại nghiêm trọng cho doanh nghiệp. Chủ đề này bao gồm từ authentication cơ bản đến encryption nâng cao.


1. Authentication Modes

Windows Authentication (Integrated Security)

Xác thực qua Active Directory/Windows Security. SQL Server tin tưởng Windows đã xác thực user — không cần lưu password trong SQL Server.

Connection String: 
Server=MyServer;Database=MyDB;Integrated Security=True;
                                                     ↑
                              Dùng Windows credentials của process đang chạy

Ưu điểm:

  • Không lưu password trong application config
  • Hỗ trợ Kerberos/NTLM, MFA qua Azure AD
  • Tự động expired/rotate theo Group Policy
  • Single Sign-On với Windows ecosystem

SQL Server Authentication

Username/Password được lưu trong SQL Server master database.

Connection String:
Server=MyServer;Database=MyDB;User Id=AppLogin;Password=Str0ng!Pass;

Khi nào cần: Non-Windows clients (Linux, Mac, containers), cross-domain scenarios, legacy applications.

Rủi ro: Password có thể bị lộ trong config files, logs, memory dumps.

Mixed Mode (cả hai)

Hỗ trợ cả Windows và SQL Server Authentication. Bật khi:

  • Có legacy apps dùng SQL Auth
  • Cần sa account (đặc biệt khi setup ban đầu)
-- Kiểm tra authentication mode
SELECT
    CASE SERVERPROPERTY('IsIntegratedSecurityOnly')
        WHEN 1 THEN 'Windows Authentication Only'
        ELSE 'Mixed Mode (Windows + SQL)'
    END AS AuthenticationMode;

-- Đổi sang Mixed Mode (cần restart SQL Server)
-- GUI: SSMS → Server Properties → Security → Server Authentication
-- Registry: HKLM\SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL16.MSSQLSERVER\MSSQLServer
-- LoginMode: 1 = Windows, 2 = Mixed

Tài khoản sa (System Administrator)

-- sa là built-in SQL Server login với sysadmin role
-- Best practices:
-- 1. Đổi tên sa (security through obscurity)
ALTER LOGIN sa WITH NAME = [SqlAdminAccount];

-- 2. Disable sa nếu không dùng
ALTER LOGIN sa DISABLE;

-- 3. Nếu cần enable, đặt strong password
ALTER LOGIN sa ENABLE;
ALTER LOGIN sa WITH PASSWORD = 'V3ry$tr0ng!RandomP@ss2024#';

2. Principals — Logins, Users, Roles

Hierarchy của Principals

SQL Server Instance Level:
├── Windows Login (DOMAIN\User, DOMAIN\Group)
├── SQL Server Login (username/password)
└── Server Roles (sysadmin, securityadmin, ...)

Database Level (per database):
├── Database User (mapped to Login)
├── Database Roles (db_owner, db_datareader, ...)
└── Application Roles (activate with password)

Tạo và Quản lý Logins

-- Tạo Windows Login
CREATE LOGIN [DOMAIN\HuyNgo] FROM WINDOWS WITH DEFAULT_DATABASE = MyDB;
CREATE LOGIN [DOMAIN\AppServiceGroup] FROM WINDOWS; -- AD Group

-- Tạo SQL Server Login
CREATE LOGIN AppLogin 
WITH PASSWORD = 'Str0ng!P@ssw0rd#2024',
     DEFAULT_DATABASE = AppDB,
     CHECK_EXPIRATION = ON,  -- Enforce password expiration  
     CHECK_POLICY = ON;      -- Enforce Windows password policy

-- Kiểm tra logins
SELECT 
    name, 
    type_desc, 
    is_disabled,
    create_date,
    password_hash IS NOT NULL AS has_password
FROM sys.server_principals
WHERE type IN ('S', 'U', 'G') -- S=SQL Login, U=Windows User, G=Windows Group
ORDER BY type, name;

Tạo và Quản lý Database Users

USE AppDB;

-- Tạo User từ Login
CREATE USER HuyNgo FOR LOGIN [DOMAIN\HuyNgo];
CREATE USER AppUser FOR LOGIN AppLogin;

-- Tạo User không có Login (contained database)
CREATE USER ContainedUser WITH PASSWORD = 'Pass!word';

-- Tạo User cho certificate/asymmetric key
CREATE USER CertUser FOR CERTIFICATE MyCert;

-- Kiểm tra users trong database
SELECT 
    u.name AS UserName,
    u.type_desc,
    l.name AS MappedLogin,
    u.default_schema_name
FROM sys.database_principals u
LEFT JOIN sys.server_principals l ON u.sid = l.sid
WHERE u.type NOT IN ('R') -- Loại bỏ roles
ORDER BY u.name;

-- Orphaned users (user không có Login tương ứng)
EXEC sp_change_users_login 'Report';
-- Fix orphaned users:
EXEC sp_change_users_login 'UPDATE_ONE', 'OrphanedUser', 'MatchingLogin';

3. Server Roles

Fixed Server Roles không thể sửa đổi (ngoại trừ user-defined server roles từ SQL 2012+):

RoleQuyền hạn
sysadminThực hiện mọi thứ — superuser
securityadminQuản lý logins, server audit specs
serveradminCấu hình server settings, shutdown
setupadminThêm/xóa linked servers
processadminKill processes bất kỳ
diskadminQuản lý disk files
dbcreatorTạo, alter, drop, restore databases
bulkadminChạy BULK INSERT
publicTất cả logins đều thuộc role này (quyền tối thiểu)
-- Thêm Login vào Server Role
ALTER SERVER ROLE sysadmin ADD MEMBER [DOMAIN\DBAdmin];
ALTER SERVER ROLE dbcreator ADD MEMBER DevLogin;

-- Xem Server Role members
SELECT 
    r.name AS role_name,
    m.name AS member_name,
    m.type_desc
FROM sys.server_role_members srm
JOIN sys.server_principals r ON srm.role_principal_id = r.principal_id
JOIN sys.server_principals m ON srm.member_principal_id = m.principal_id
ORDER BY r.name, m.name;

-- User-defined Server Role (SQL 2012+)
CREATE SERVER ROLE ReadonlyDBA;
GRANT VIEW SERVER STATE TO ReadonlyDBA;
GRANT VIEW ANY DATABASE TO ReadonlyDBA;
ALTER SERVER ROLE ReadonlyDBA ADD MEMBER MonitorLogin;

4. Database Roles

Fixed Database Roles

RoleQuyền hạn
db_ownerFull control database
db_securityadminQuản lý roles và permissions trong db
db_accessadminThêm/xóa Windows và SQL logins
db_backupoperatorBackup database
db_ddladminTạo/sửa/xóa schema objects
db_datawriterINSERT, UPDATE, DELETE trên tất cả tables
db_datareaderSELECT trên tất cả tables
db_denydatawriterDENY INSERT, UPDATE, DELETE
db_denydatareaderDENY SELECT
publicTất cả users đều thuộc role này
-- Thêm User vào Database Role
ALTER ROLE db_datareader ADD MEMBER AppUser;
ALTER ROLE db_datawriter ADD MEMBER AppUser;

-- Tạo Custom Database Role (Principle of Least Privilege)
CREATE ROLE OrderManagers;
GRANT SELECT, INSERT, UPDATE ON dbo.Orders TO OrderManagers;
GRANT SELECT, INSERT ON dbo.OrderDetails TO OrderManagers;
GRANT EXECUTE ON dbo.CreateOrder TO OrderManagers;
GRANT EXECUTE ON dbo.UpdateOrderStatus TO OrderManagers;

ALTER ROLE OrderManagers ADD MEMBER UserA;
ALTER ROLE OrderManagers ADD MEMBER UserB;

-- Xem Role members
SELECT 
    r.name AS role_name,
    m.name AS member_name
FROM sys.database_role_members drm
JOIN sys.database_principals r ON drm.role_principal_id = r.principal_id
JOIN sys.database_principals m ON drm.member_principal_id = m.principal_id
ORDER BY r.name, m.name;

5. Object Permissions — GRANT, DENY, REVOKE

Cú pháp cơ bản

-- GRANT permissions
GRANT SELECT ON dbo.Products TO AppUser;
GRANT SELECT, INSERT, UPDATE, DELETE ON dbo.Orders TO AppUser;
GRANT EXECUTE ON dbo.GetOrderDetails TO AppUser;
GRANT ALTER ON SCHEMA::dbo TO PowerUser;

-- GRANT WITH GRANT OPTION (cho phép user gán quyền cho người khác)
GRANT SELECT ON dbo.Reports TO ManagerUser WITH GRANT OPTION;

-- DENY (override mọi GRANT)
DENY DELETE ON dbo.Orders TO AppUser;         -- Không thể DELETE
DENY SELECT ON dbo.SalaryTable TO AllStaff;   -- Không ai đọc được

-- REVOKE (xóa GRANT hoặc DENY)
REVOKE SELECT ON dbo.Products FROM AppUser;
REVOKE DELETE ON dbo.Orders FROM AppUser; -- Xóa DENY, nhưng vẫn không có GRANT

Kiểm tra Permissions

-- Permissions của một user
SELECT 
    dp.permission_name,
    dp.state_desc,     -- GRANT, DENY, REVOKE
    dp.class_desc,
    OBJECT_NAME(dp.major_id) AS object_name,
    sp.name AS principal_name
FROM sys.database_permissions dp
JOIN sys.database_principals sp ON dp.grantee_principal_id = sp.principal_id
WHERE sp.name = 'AppUser'
ORDER BY dp.class_desc, dp.permission_name;

-- Effective permissions của current user
SELECT * FROM fn_my_permissions(NULL, 'DATABASE');
SELECT * FROM fn_my_permissions('dbo.Orders', 'OBJECT');

-- HAS_PERMS_BY_NAME để kiểm tra trong code
IF HAS_PERMS_BY_NAME('dbo.Orders', 'OBJECT', 'SELECT') = 1
    PRINT 'Can SELECT from Orders';

6. Schema-based Permissions

Schema là namespace chứa objects. GRANT quyền trên schema = tự động có quyền trên tất cả objects hiện tại và tương lai trong schema đó.

-- Tạo schemas theo nhóm chức năng
CREATE SCHEMA Sales AUTHORIZATION dbo;
CREATE SCHEMA HR AUTHORIZATION dbo;
CREATE SCHEMA Finance AUTHORIZATION dbo;
CREATE SCHEMA Reporting AUTHORIZATION dbo;

-- Objects trong schemas
CREATE TABLE Sales.Orders (...);
CREATE TABLE Sales.Customers (...);
CREATE TABLE HR.Employees (...);
CREATE TABLE Finance.Payroll (...);

-- Permission model đơn giản, dễ quản lý:
-- App login chỉ cần Sales schema
GRANT SELECT, INSERT, UPDATE, DELETE ON SCHEMA::Sales TO SalesAppUser;
GRANT EXECUTE ON SCHEMA::Sales TO SalesAppUser;

-- Report login chỉ đọc Reporting schema
GRANT SELECT ON SCHEMA::Reporting TO ReportUser;

-- HR admin
GRANT SELECT, INSERT, UPDATE, DELETE ON SCHEMA::HR TO HRAppUser;
DENY SELECT ON HR.Employees TO FinanceUser; -- Tường minh deny

-- Xem tất cả schemas và owners
SELECT name, principal_id, SCHEMA_NAME(schema_id) 
FROM sys.schemas
ORDER BY name;

7. Row-Level Security (RLS)

RLS cho phép kiểm soát access ở cấp row dựa trên security predicate function.

Filter Predicate (Chỉ thấy rows được phép)

-- Ví dụ: Sales rep chỉ thấy orders được assign cho họ
CREATE TABLE Sales.Orders (
    OrderId INT PRIMARY KEY,
    CustomerId INT,
    SalesRepLogin NVARCHAR(100),
    Amount DECIMAL(10,2),
    OrderDate DATE
);

-- Security predicate function
CREATE FUNCTION Security.fn_OrderAccess(@SalesRepLogin NVARCHAR(100))
RETURNS TABLE
WITH SCHEMABINDING
AS RETURN
    SELECT 1 AS allowed
    WHERE 
        @SalesRepLogin = USER_NAME()      -- Chính user đó
        OR IS_ROLEMEMBER('db_owner') = 1  -- Hoặc admin
        OR IS_ROLEMEMBER('SalesManager') = 1; -- Hoặc manager

-- Tạo Security Policy
CREATE SECURITY POLICY OrderFilterPolicy
ADD FILTER PREDICATE Security.fn_OrderAccess(SalesRepLogin) 
    ON Sales.Orders
WITH (STATE = ON, SCHEMABINDING = ON);

-- Test:
EXECUTE AS USER = 'rep_nguyen';
SELECT * FROM Sales.Orders; -- Chỉ thấy orders của rep_nguyen
REVERT;

Block Predicate (Ngăn INSERT/UPDATE vi phạm policy)

-- Block predicate: Ngăn insert row với SalesRepLogin khác với current user
CREATE SECURITY POLICY OrderFilterPolicy
ADD FILTER PREDICATE Security.fn_OrderAccess(SalesRepLogin) ON Sales.Orders,
ADD BLOCK PREDICATE Security.fn_OrderAccess(SalesRepLogin) ON Sales.Orders 
    AFTER INSERT, -- Ngăn insert rows không thuộc về họ
    AFTER UPDATE  -- Ngăn update để chuyển ownership
WITH (STATE = ON);

-- Không cần modify queries trong application!
-- Tất cả SELECT/INSERT/UPDATE tự động được filter

RLS với Session Context

-- Dùng Session Context cho multi-tenant (tốt hơn USER_NAME() khi app dùng connection pool)
CREATE FUNCTION dbo.fn_TenantFilter(@TenantId INT)
RETURNS TABLE WITH SCHEMABINDING AS RETURN
    SELECT 1 AS OK
    WHERE @TenantId = CONVERT(INT, SESSION_CONTEXT(N'TenantId'))
        OR IS_MEMBER('db_owner') = 1;

CREATE SECURITY POLICY TenantIsolationPolicy
ADD FILTER PREDICATE dbo.fn_TenantFilter(TenantId) ON dbo.Orders,
ADD BLOCK PREDICATE dbo.fn_TenantFilter(TenantId) ON dbo.Orders
WITH (STATE = ON);

-- Application set context trước khi query (sau login):
EXEC sys.sp_set_session_context 
    @key = N'TenantId', 
    @value = 42,
    @read_only = 1; -- Ngăn application tự thay đổi TenantId!

8. Dynamic Data Masking (DDM)

DDM che giấu dữ liệu nhạy cảm khi query — không thay đổi dữ liệu thực trong storage.

Masking Functions

CREATE TABLE dbo.CustomerPII (
    CustomerId    INT PRIMARY KEY,
    FullName      NVARCHAR(100), -- Không mask
    
    -- Email: hiXX@XXXX.com
    Email         NVARCHAR(200) MASKED WITH (FUNCTION = 'email()'),
    
    -- Che giấu hoàn toàn: xxxx
    Password      NVARCHAR(100) MASKED WITH (FUNCTION = 'default()'),
    
    -- Chỉ hiện 2 ký tự đầu và 2 cuối: "Hu...go"
    NickName      NVARCHAR(50)  MASKED WITH (FUNCTION = 'partial(2,"...",2)'),
    
    -- Số điện thoại: XXX-XXX-1234
    Phone         NVARCHAR(20)  MASKED WITH (FUNCTION = 'partial(0,"XXX-XXX-",4)'),
    
    -- Credit card: XXXX-XXXX-XXXX-5678
    CreditCard    NVARCHAR(20)  MASKED WITH (FUNCTION = 'partial(0,"XXXX-XXXX-XXXX-",4)'),
    
    -- Random number trong khoảng
    FakeIncome    DECIMAL(10,2) MASKED WITH (FUNCTION = 'random(1000, 9999)'),
    
    -- Date: 01.01.1900
    BirthDate     DATE          MASKED WITH (FUNCTION = 'default()')
);

-- Thêm mask vào cột đã có:
ALTER TABLE dbo.CustomerPII
ALTER COLUMN SSN NVARCHAR(11) MASKED WITH (FUNCTION = 'partial(0,"XXX-XX-",4)');

-- Bỏ mask:
ALTER TABLE dbo.CustomerPII ALTER COLUMN Email DROP MASKED;

-- UNMASK permission:
GRANT UNMASK TO PowerUserRole;           -- Toàn bộ database
GRANT UNMASK ON dbo.CustomerPII TO AdminUser; -- Chỉ table đó

-- Test:
EXECUTE AS USER = 'NormalUser';
SELECT CustomerId, FullName, Email, Phone, CreditCard FROM dbo.CustomerPII;
-- Email: aXX@XXXX.com, Phone: XXX-XXX-5678, CreditCard: XXXX-XXXX-XXXX-9012
REVERT;

EXECUTE AS USER = 'AdminUser';
SELECT CustomerId, FullName, Email, Phone, CreditCard FROM dbo.CustomerPII;
-- Thấy dữ liệu thật
REVERT;

9. Always Encrypted

Always Encrypted đảm bảo chỉ client application mới có thể đọc dữ liệu nhạy cảm — SQL Server, DBA, Cloud admin đều không đọc được. Encryption/Decryption xảy ra hoàn toàn ở phía client.

Kiến trúc

Client Application
  ├── Column Master Key (CMK): Lưu trong Key Vault / Windows Cert Store
  └── Column Encryption Key (CEK): Được encrypt bởi CMK, lưu trong SQL Server

SQL Server
  └── Chỉ thấy ciphertext (bytes) — không có CMK, không decrypt được

Setup Always Encrypted

-- Step 1: Tạo Column Master Key metadata (key thực lưu ở Key Vault/cert store)
CREATE COLUMN MASTER KEY MyCMK
WITH (
    KEY_STORE_PROVIDER_NAME = 'AZURE_KEY_VAULT',
    KEY_PATH = 'https://mykeyvault.vault.azure.net/keys/MyCMK/abc123'
);
-- Hoặc Windows Certificate Store:
CREATE COLUMN MASTER KEY MyCMK
WITH (
    KEY_STORE_PROVIDER_NAME = 'MSSQL_CERTIFICATE_STORE',
    KEY_PATH = 'LocalMachine/My/ABC123DEF456...'
);

-- Step 2: Tạo Column Encryption Key (encrypted bằng CMK)
CREATE COLUMN ENCRYPTION KEY MyCEK
WITH VALUES (
    COLUMN_MASTER_KEY = MyCMK,
    ALGORITHM = 'RSA_OAEP',
    ENCRYPTED_VALUE = 0x01234567... -- Giá trị encrypted
);

-- Step 3: Tạo table với encrypted columns
CREATE TABLE dbo.Patients (
    PatientId    INT PRIMARY KEY,
    Name         NVARCHAR(100), -- Không encrypt
    SSN          CHAR(11) ENCRYPTED WITH (
        COLUMN_ENCRYPTION_KEY = MyCEK,
        ENCRYPTION_TYPE = Deterministic, -- Cho phép equality search
        ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256'
    ),
    Diagnosis    NVARCHAR(500) ENCRYPTED WITH (
        COLUMN_ENCRYPTION_KEY = MyCEK,
        ENCRYPTION_TYPE = Randomized, -- Bảo mật hơn, không search được
        ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256'
    )
);

Deterministic vs Randomized Encryption

DeterministicRandomized
Cùng plaintext → cùng ciphertext?✅ Có❌ Không
Hỗ trợ equality searchWHERE SSN = @ssn❌ Không
Hỗ trợ JOIN✅ Có❌ Không
Bảo mậtThấp hơn (có thể infer patterns)Cao hơn

10. Transparent Data Encryption (TDE)

TDE mã hóa toàn bộ database files (data, log, backup) ở tầng storage. Hoàn toàn transparent với application.

Setup TDE

-- Step 1: Tạo Database Master Key trong master database
USE master;
CREATE MASTER KEY ENCRYPTION BY PASSWORD = 'Str0ng#MasterKey!Pass';

-- Step 2: Tạo Certificate để bảo vệ TDE key
CREATE CERTIFICATE TDECert 
WITH SUBJECT = 'TDE Certificate for AppDB',
     EXPIRY_DATE = '2099-12-31';

-- ⚠️ QUAN TRỌNG: Backup certificate ngay! Nếu mất certificate, mất dữ liệu!
BACKUP CERTIFICATE TDECert 
TO FILE = 'C:\Backups\TDECert.cer'
WITH PRIVATE KEY (
    FILE = 'C:\Backups\TDECert_Key.pvk',
    ENCRYPTION BY PASSWORD = 'BackupKeyPassword!2024'
);

-- Step 3: Tạo Database Encryption Key trong database cần encrypt
USE AppDB;
CREATE DATABASE ENCRYPTION KEY
WITH ALGORITHM = AES_256
ENCRYPTION BY SERVER CERTIFICATE TDECert;

-- Step 4: Bật TDE
ALTER DATABASE AppDB SET ENCRYPTION ON;

-- Monitor tiến độ encryption
SELECT 
    DB_NAME(database_id) AS db_name,
    encryption_state,
    CASE encryption_state
        WHEN 0 THEN 'No encryption'
        WHEN 1 THEN 'Unencrypted'
        WHEN 2 THEN 'Encryption in progress'
        WHEN 3 THEN 'Encrypted'
        WHEN 4 THEN 'Key change in progress'
        WHEN 5 THEN 'Decryption in progress'
        WHEN 6 THEN 'Protection change in progress'
    END AS encryption_state_desc,
    percent_complete,
    encryptor_thumbprint
FROM sys.dm_database_encryption_keys;

Khôi phục TDE Database sang server khác

-- Trên server đích: Phải restore certificate TRƯỚC khi restore database
USE master;
CREATE MASTER KEY ENCRYPTION BY PASSWORD = 'NewServerMasterKeyPass';

CREATE CERTIFICATE TDECert
FROM FILE = 'C:\Backups\TDECert.cer'
WITH PRIVATE KEY (
    FILE = 'C:\Backups\TDECert_Key.pvk',
    DECRYPTION BY PASSWORD = 'BackupKeyPassword!2024'
);

-- Bây giờ mới có thể restore database
RESTORE DATABASE AppDB FROM DISK = '...';

11. SQL Server Audit

Audit tracks các hành động trong SQL Server và ghi vào file, Windows Event Log, hoặc Application Log.

Server Audit và Audit Specification

-- Bước 1: Tạo Server Audit (cấu hình destination)
CREATE SERVER AUDIT ProductionAudit
TO FILE (
    FILEPATH = 'D:\SQLAudit\',
    MAXSIZE = 100 MB,
    MAX_FILES = 50,             -- Giữ 50 files
    RESERVE_DISK_SPACE = OFF
)
WITH (
    QUEUE_DELAY = 1000,         -- ms, 0 = synchronous
    ON_FAILURE = CONTINUE,      -- CONTINUE hoặc SHUTDOWN hoặc FAIL_OPERATION
    AUDIT_GUID = NEWID()
);

ALTER SERVER AUDIT ProductionAudit WITH (STATE = ON);

-- Bước 2a: Server Audit Specification (server-level events)
CREATE SERVER AUDIT SPECIFICATION ServerSecurityAudit
FOR SERVER AUDIT ProductionAudit
    ADD (FAILED_LOGIN_GROUP),               -- Đăng nhập thất bại
    ADD (SUCCESSFUL_LOGIN_GROUP),           -- Đăng nhập thành công
    ADD (LOGOUT_GROUP),                     -- Đăng xuất
    ADD (LOGIN_CHANGE_PASSWORD_GROUP),      -- Đổi password
    ADD (SERVER_ROLE_MEMBER_CHANGE_GROUP),  -- Thay đổi server role
    ADD (CREATE_LOGIN_GROUP),               -- Tạo login
    ADD (ALTER_LOGIN_GROUP),                -- Sửa login
    ADD (DROP_LOGIN_GROUP)                  -- Xóa login
WITH (STATE = ON);

-- Bước 2b: Database Audit Specification (database-level events)
USE SensitiveDatabase;
CREATE DATABASE AUDIT SPECIFICATION DataChangeAudit
FOR SERVER AUDIT ProductionAudit
    ADD (SELECT ON dbo.Payroll BY public),         -- Ai đọc Payroll
    ADD (INSERT, UPDATE, DELETE ON dbo.Orders BY public), -- Thay đổi Orders
    ADD (EXECUTE ON SCHEMA::dbo BY public),        -- Gọi stored procedures
    ADD (DATABASE_OBJECT_CHANGE_GROUP),            -- DDL changes
    ADD (SCHEMA_OBJECT_PERMISSION_CHANGE_GROUP)    -- Permission changes
WITH (STATE = ON);

Xem Audit Logs

-- Đọc từ file
SELECT TOP 100
    event_time AT TIME ZONE 'SE Asia Standard Time' AS event_time_local,
    action_id,
    succeeded,
    session_server_principal_name AS login_name,
    server_instance_name,
    database_name,
    schema_name,
    object_name,
    statement,
    additional_information
FROM sys.fn_get_audit_file(
    'D:\SQLAudit\ProductionAudit_*.sqlaudit',
    DEFAULT,
    DEFAULT
)
WHERE action_id IN ('SL', 'IN', 'UP', 'DL') -- SELECT, INSERT, UPDATE, DELETE
ORDER BY event_time DESC;

-- Filter theo user
SELECT *
FROM sys.fn_get_audit_file('D:\SQLAudit\*.sqlaudit', DEFAULT, DEFAULT)
WHERE session_server_principal_name = 'SuspiciousUser'
ORDER BY event_time;

12. EXECUTE AS — Impersonation

-- Impersonation tạm thời trong session
EXECUTE AS USER = 'LimitedUser';
    SELECT * FROM dbo.Orders; -- Thực thi với quyền LimitedUser
    -- Nếu LimitedUser không có quyền → Error
REVERT; -- Quay lại context gốc (bắt buộc!)

-- EXECUTE AS trong Stored Procedure
CREATE PROCEDURE dbo.GetRestrictedData
WITH EXECUTE AS OWNER  -- Chạy dưới ngữ cảnh của owner (thường là dbo)
AS
BEGIN
    -- Caller không cần quyền SELECT trực tiếp trên bảng này
    SELECT SensitiveColumn FROM dbo.ProtectedTable;
END;
-- Cấp cho user quyền EXECUTE, không cần SELECT:
GRANT EXECUTE ON dbo.GetRestrictedData TO ReportUser;

-- EXECUTE AS SELF: chạy dưới context của người tạo procedure
-- EXECUTE AS USER = 'specific_user': context cố định
-- EXECUTE AS CALLER: context của người gọi (default)

-- Kiểm tra context hiện tại:
SELECT ORIGINAL_LOGIN() AS original_login,
       USER_NAME() AS current_context,
       SYSTEM_USER AS system_login;

13. SQL Injection Prevention

Các hình thức SQL Injection

-- 1. Classic string injection
-- Input: ' OR '1'='1
-- Query: WHERE Password = '' OR '1'='1' -- Luôn đúng!

-- 2. UNION-based injection
-- Input: ' UNION SELECT username, password FROM sys.sql_logins --
-- Lấy thông tin hệ thống!

-- 3. Stacked queries
-- Input: '; DROP TABLE Users; --
-- Xóa toàn bộ table!

-- 4. Time-based blind injection
-- Input: ' IF (1=1) WAITFOR DELAY '0:0:5' --
-- Detect database structure qua timing

Phòng chống

-- ✅ CÁCH 1: Parameterized queries (bắt buộc!)
-- Application code dùng SqlParameters (C#):
-- cmd.Parameters.AddWithValue("@Name", userInput);

-- ✅ CÁCH 2: sp_executesql với parameters
DECLARE @sql NVARCHAR(500); 
DECLARE @userName NVARCHAR(100) = ''; -- Từ user input

SET @sql = N'SELECT * FROM Users WHERE UserName = @name';
EXEC sp_executesql @sql, N'@name NVARCHAR(100)', @name = @userName;
-- @userName không được interpret là SQL, dù chứa SQL code

-- ✅ CÁCH 3: Stored Procedures
CREATE PROCEDURE dbo.GetUser @UserName NVARCHAR(100)
AS
    SELECT UserId, UserName, Email -- Chỉ expose cần thiết
    FROM dbo.Users
    WHERE UserName = @UserName; -- Safe

-- ✅ CÁCH 4: QUOTENAME cho dynamic object names
DECLARE @TableName NVARCHAR(100) = 'Orders'; -- Từ whitelist
IF @TableName NOT IN ('Orders', 'Products', 'Customers')
    THROW 50001, 'Invalid table', 1;
    
DECLARE @sql NVARCHAR(500) = N'SELECT * FROM ' + QUOTENAME(@TableName);
EXEC sp_executesql @sql;

-- ✅ CÁCH 5: Principle of Least Privilege
-- Application login chỉ có SELECT, INSERT, UPDATE — không DROP, không TRUNCATE
-- Không dùng sa hoặc sysadmin cho application

-- ❌ NEVER: String concatenation với user input
DECLARE @badSql NVARCHAR(500) = 
    'SELECT * FROM Users WHERE Name = ''' + @userInput + ''''; -- VULNERABLE!
EXEC(@badSql);

14. Best Practices Bảo mật Tổng hợp

Checklist Bảo mật Production

-- 1. Vô hiệu hóa tài khoản không dùng
ALTER LOGIN sa DISABLE;
ALTER LOGIN GuestLogin DISABLE;

-- 2. Kiểm tra guest user trong databases
SELECT name FROM sys.databases WHERE is_trustworthy_on = 1; -- Phải là ít
-- Tắt TRUSTWORTHY nếu không cần:
ALTER DATABASE MyDB SET TRUSTWORTHY OFF;

-- 3. Kiểm tra ai có sysadmin
SELECT p.name, p.type_desc
FROM sys.server_principals p
JOIN sys.server_role_members rm ON p.principal_id = rm.member_principal_id
JOIN sys.server_principals r ON rm.role_principal_id = r.principal_id
WHERE r.name = 'sysadmin';

-- 4. Kiểm tra SQL Logins (nên minimize)
SELECT name, is_disabled FROM sys.sql_logins ORDER BY name;

-- 5. Permissions report
SELECT
    dp.state_desc AS PermissionState,
    dp.permission_name,
    dp.class_desc,
    COALESCE(OBJECT_NAME(dp.major_id), 
             SCHEMA_NAME(dp.major_id), 
             DB_NAME(dp.major_id)) AS SecurableName,
    pr.name AS GranteeName,
    pr.type_desc AS GranteeType
FROM sys.database_permissions dp
JOIN sys.database_principals pr ON dp.grantee_principal_id = pr.principal_id
WHERE dp.state_desc IN ('GRANT', 'DENY')
ORDER BY pr.name, dp.permission_name;

Summary Best Practices

AreaBest Practice
AuthenticationWindows Auth, disable sa, strong passwords
AuthorizationPrinciple of Least Privilege, custom roles per function
Encryption at restTDE cho production databases
Encryption in transitForce Encrypted connections (ssl)
Sensitive columnsAlways Encrypted hoặc DDM
Access controlRLS cho row-level isolation
SQL InjectionParameterized queries, sp_executesql, stored procs
AuditingSQL Server Audit cho login failures + sensitive data access
MonitoringRegular permission reviews, alert on sysadmin changes
BackupEncrypt backups (WITH ENCRYPTION), backup TDE certificates

Backup & Recovery

Recovery Models

SQL Server cung cấp 3 recovery model, mỗi loại ảnh hưởng đến cách transaction log được quản lý và loại backup có thể thực hiện.

So sánh Recovery Models

Recovery ModelLog TruncationTransaction Log BackupPoint-in-time Recovery
SIMPLEAutomatic (checkpoint)Không hỗ trợKhông
FULLChỉ sau log backupBắt buộc
BULK-LOGGEDChỉ sau log backupHạn chế (không qua bulk ops)

SIMPLE Recovery Model

  • Transaction log được truncate tự động sau mỗi checkpoint
  • Không thể thực hiện transaction log backup
  • Thích hợp: development, non-critical databases, data warehouses (ETL chạy từ đầu được)
  • Rủi ro: mất dữ liệu từ lần full/differential backup cuối cùng
-- Xem recovery model hiện tại
SELECT name, recovery_model_desc 
FROM sys.databases 
WHERE name = 'AdventureWorks';

-- Chuyển sang SIMPLE
ALTER DATABASE AdventureWorks SET RECOVERY SIMPLE;

FULL Recovery Model

  • Tất cả transactions được ghi đầy đủ vào log
  • Transaction log KHÔNG tự truncate → bắt buộc phải backup log thường xuyên
  • Cho phép point-in-time recovery
  • Thích hợp: production OLTP systems, khi RPO thấp (mất ít dữ liệu nhất)
ALTER DATABASE AdventureWorks SET RECOVERY FULL;

BULK-LOGGED Recovery Model

  • Minimally log các bulk operations: BULK INSERT, SELECT INTO, index rebuild, BCP
  • Khi có bulk operation: point-in-time restore không khả dụng trong khoảng đó
  • Log backup vẫn cần thiết
  • Thích hợp: temporary switch trong ETL loads để giảm log space
-- Switch tạm để load bulk data
ALTER DATABASE AdventureWorks SET RECOVERY BULK_LOGGED;
-- ... thực hiện bulk operations ...
ALTER DATABASE AdventureWorks SET RECOVERY FULL;
BACKUP LOG AdventureWorks TO DISK = 'log_after_bulk.bak';

Các loại Backup

1. Full Backup

Backup toàn bộ database tại một thời điểm. Là nền tảng của mọi backup strategy.

-- Full backup ra file local
BACKUP DATABASE AdventureWorks
TO DISK = 'D:\Backups\AdventureWorks_Full_20260401.bak'
WITH 
    NAME = 'Full Backup - AdventureWorks',
    DESCRIPTION = 'Weekly full backup',
    COMPRESSION,
    CHECKSUM,
    STATS = 10;  -- Báo tiến độ mỗi 10%

-- Full backup ra nhiều file (striped) để tăng tốc
BACKUP DATABASE AdventureWorks
TO 
    DISK = 'D:\Backups\AW_1of2.bak',
    DISK = 'D:\Backups\AW_2of2.bak'
WITH COMPRESSION, STATS = 10;

2. Differential Backup

Backup tất cả các trang đã thay đổi kể từ lần full backup cuối cùng.

  • Nhanh hơn full backup, nhỏ hơn
  • Khi restore: cần full backup + differential backup mới nhất
  • Không dựa trên differential backup trước
BACKUP DATABASE AdventureWorks
TO DISK = 'D:\Backups\AdventureWorks_Diff_20260401.bak'
WITH DIFFERENTIAL,
    NAME = 'Differential Backup - AdventureWorks',
    COMPRESSION,
    CHECKSUM;

3. Transaction Log Backup

Backup phần transaction log chưa được backup, cho phép point-in-time recovery.

  • Yêu cầu: Recovery model = FULL hoặc BULK-LOGGED
  • Sau khi backup: log được truncate (đánh dấu có thể tái sử dụng)
  • Phải có ít nhất 1 full backup trước khi backup log lần đầu
-- Log backup
BACKUP LOG AdventureWorks
TO DISK = 'D:\Backups\AdventureWorks_Log_20260401_1400.trn'
WITH 
    NAME = 'Log Backup 14:00',
    COMPRESSION,
    CHECKSUM;

-- Backup tất cả log files (để clear log space khẩn cấp)
BACKUP LOG AdventureWorks TO DISK = 'NUL';  -- KHÔNG dùng trong production thực tế!

4. File/Filegroup Backup

Backup từng file hoặc filegroup riêng lẻ — hữu ích cho các database rất lớn (VLDB).

-- Backup một filegroup cụ thể
BACKUP DATABASE AdventureWorks
FILEGROUP = 'PRIMARY'
TO DISK = 'D:\Backups\AW_PRIMARY_FG.bak'
WITH COMPRESSION;

-- Backup một file cụ thể
BACKUP DATABASE AdventureWorks
FILE = 'AdventureWorks_Data'
TO DISK = 'D:\Backups\AW_DataFile.bak'
WITH COMPRESSION;

5. Copy-Only Backup

Backup không ảnh hưởng đến backup chain — không reset differential base.

-- Copy-only full backup (không làm hỏng backup chain)
BACKUP DATABASE AdventureWorks
TO DISK = 'D:\Backups\AdventureWorks_CopyOnly.bak'
WITH COPY_ONLY, COMPRESSION;

-- Copy-only log backup (không truncate log)
BACKUP LOG AdventureWorks
TO DISK = 'D:\Backups\AdventureWorks_Log_CopyOnly.trn'
WITH COPY_ONLY;

Backup Destinations

Backup ra Disk (Local / Network Share)

-- Local disk
BACKUP DATABASE AdventureWorks
TO DISK = 'D:\Backups\AdventureWorks.bak'
WITH COMPRESSION;

-- Network share (UNC path)
BACKUP DATABASE AdventureWorks
TO DISK = '\\BackupServer\SQLBackups\AdventureWorks.bak'
WITH COMPRESSION;

Backup lên Azure Blob Storage (URL)

-- Tạo credential trước
CREATE CREDENTIAL [https://mystorageaccount.blob.core.windows.net/backups]
WITH IDENTITY = 'SHARED ACCESS SIGNATURE',
SECRET = 'sv=2020-08...&sig=xxxxx';

-- Backup lên Azure
BACKUP DATABASE AdventureWorks
TO URL = 'https://mystorageaccount.blob.core.windows.net/backups/AdventureWorks.bak'
WITH CREDENTIAL = 'https://mystorageaccount.blob.core.windows.net/backups',
    COMPRESSION, ENCRYPTION
    (ALGORITHM = AES_256, SERVER CERTIFICATE = BackupEncryptCert);

Restore Operations

RESTORE với RECOVERY vs NORECOVERY

OptionTrạng thái sau restoreKhi nào dùng
WITH RECOVERYDatabase ONLINE, nhận connectionsRestore cuối cùng trong chuỗi
WITH NORECOVERYDatabase RESTORING, không nhận connectionsKhi còn cần apply thêm backup
WITH STANDBYDatabase online read-onlyLog shipping secondary
-- Restore full backup, chưa xong
RESTORE DATABASE AdventureWorks
FROM DISK = 'D:\Backups\AdventureWorks_Full.bak'
WITH NORECOVERY,
    MOVE 'AdventureWorks_Data' TO 'D:\SQL\AW_Data.mdf',
    MOVE 'AdventureWorks_Log' TO 'D:\SQL\AW_Log.ldf',
    STATS = 10;

-- Apply differential backup, chưa xong
RESTORE DATABASE AdventureWorks
FROM DISK = 'D:\Backups\AdventureWorks_Diff.bak'
WITH NORECOVERY;

-- Apply log backup, chưa xong
RESTORE LOG AdventureWorks
FROM DISK = 'D:\Backups\AdventureWorks_Log_1.trn'
WITH NORECOVERY;

-- Apply log backup cuối, bring database ONLINE
RESTORE LOG AdventureWorks
FROM DISK = 'D:\Backups\AdventureWorks_Log_2.trn'
WITH RECOVERY;

Point-in-Time Recovery

-- Restore đến một thời điểm cụ thể (STOPAT)
RESTORE LOG AdventureWorks
FROM DISK = 'D:\Backups\AdventureWorks_Log.trn'
WITH RECOVERY,
    STOPAT = '2026-04-01T14:35:00';

-- Restore đến một LSN (Log Sequence Number) cụ thể
RESTORE LOG AdventureWorks
FROM DISK = 'D:\Backups\AdventureWorks_Log.trn'
WITH RECOVERY,
    STOPATMARK = 'lsn:0x00000028:00000210:0001';

-- Restore đến một named transaction mark
RESTORE LOG AdventureWorks
FROM DISK = 'D:\Backups\AdventureWorks_Log.trn'
WITH RECOVERY,
    STOPATMARK = 'MyTransactionMark';

Tail-Log Backup (quan trọng trước restore!)

Backup phần log chưa được backup trước khi restore — để không mất dữ liệu từ lần log backup cuối.

-- Tail-log backup khi database còn accessible
BACKUP LOG AdventureWorks
TO DISK = 'D:\Backups\AdventureWorks_TailLog.trn'
WITH NORECOVERY,  -- Database chuyển sang RESTORING ngay sau đó
    NO_TRUNCATE;  -- Nếu data files bị hỏng

-- Tail-log backup khi data files bị hỏng (log còn intact)
BACKUP LOG AdventureWorks
TO DISK = 'D:\Backups\AdventureWorks_TailLog.trn'
WITH NO_TRUNCATE, NORECOVERY;

Page-Level Restore

Restore từng trang bị hỏng mà không cần offline toàn bộ database (chỉ FULL recovery model).

-- Xem các trang bị hỏng
SELECT * FROM msdb.dbo.suspect_pages;

-- Restore từng trang (database vẫn ONLINE, các trang khác accessible)
RESTORE DATABASE AdventureWorks
PAGE = '1:57, 1:202, 1:916'
FROM DISK = 'D:\Backups\AdventureWorks_Full.bak'
WITH NORECOVERY;

-- Apply log sau đó để recover
RESTORE LOG AdventureWorks
FROM DISK = 'D:\Backups\AdventureWorks_Log.trn'
WITH RECOVERY;

RPO và RTO

Recovery Point Objective (RPO)

Mất tối đa bao nhiêu dữ liệu? — đo bằng thời gian

  • RPO = 1 giờ → có thể mất tối đa 1 giờ dữ liệu
  • Đạt được bằng cách: backup log thường xuyên, Always On AG synchronous

Recovery Time Objective (RTO)

Phục hồi trong bao lâu? — đo bằng thời gian downtime

  • RTO = 30 phút → database phải online lại trong 30 phút
  • Đạt được bằng cách: Always On AG (auto failover ~30s), pre-staged restore

Tradeoff RPO vs RTO

Giải phápRPORTOChi phí
Full backup hàng tuần~7 ngàyGiờThấp
Full + Diff + Log hàng giờ~1 giờ30-60 phútTrung bình
Always On AG Sync~0 (zero data loss)< 1 phútCao
Always On AG AsyncVài giây< 1 phútCao

Backup Strategy điển hình

Strategy cho OLTP Production

Thứ Hai 00:00  → Full Backup
Thứ Ba đến CN 00:00 → Differential Backup
Mỗi giờ (00:00 - 23:00) → Transaction Log Backup
-- Job: Weekly Full Backup (chạy Chủ Nhật 00:00)
BACKUP DATABASE AdventureWorks
TO DISK = N'D:\Backups\AW_Full_' + 
    CONVERT(VARCHAR, GETDATE(), 112) + '.bak'
WITH COMPRESSION, CHECKSUM, STATS = 10;

-- Job: Nightly Differential (Thứ Hai - Thứ Bảy 00:00)
BACKUP DATABASE AdventureWorks
TO DISK = N'D:\Backups\AW_Diff_' + 
    CONVERT(VARCHAR, GETDATE(), 112) + '_' +
    REPLACE(CONVERT(VARCHAR, GETDATE(), 108), ':', '') + '.bak'
WITH DIFFERENTIAL, COMPRESSION, CHECKSUM;

-- Job: Hourly Log Backup
BACKUP LOG AdventureWorks
TO DISK = N'D:\Backups\AW_Log_' +
    CONVERT(VARCHAR, GETDATE(), 112) + '_' +
    REPLACE(CONVERT(VARCHAR, GETDATE(), 108), ':', '') + '.trn'
WITH COMPRESSION, CHECKSUM;

Xác minh và Kiểm tra Backup

RESTORE VERIFYONLY

Kiểm tra backup có readable và không bị corrupt — không thực sự restore.

RESTORE VERIFYONLY
FROM DISK = 'D:\Backups\AdventureWorks_Full.bak'
WITH CHECKSUM;  -- Xác minh checksum nếu backup có checksum

DBCC CHECKDB

Kiểm tra tính integrity của database sau khi restore.

-- Kiểm tra toàn bộ database
DBCC CHECKDB ('AdventureWorks') WITH NO_INFOMSGS, ALL_ERRORMSGS;

-- Kiểm tra một table cụ thể
DBCC CHECKTABLE ('Sales.Orders') WITH NO_INFOMSGS;

-- Xem thông tin backup history
SELECT 
    bs.database_name,
    bs.backup_start_date,
    bs.backup_finish_date,
    bs.type,  -- D=Full, I=Differential, L=Log
    CAST(bs.backup_size / 1024.0 / 1024 AS DECIMAL(10,2)) AS backup_size_mb,
    bmf.physical_device_name
FROM msdb.dbo.backupset bs
JOIN msdb.dbo.backupmediafamily bmf ON bs.media_set_id = bmf.media_set_id
WHERE bs.database_name = 'AdventureWorks'
ORDER BY bs.backup_start_date DESC;

Backup Compression và Encryption

Backup Compression

-- Enable compression mặc định ở server level
EXEC sp_configure 'backup compression default', 1;
RECONFIGURE;

-- Compression cho từng backup
BACKUP DATABASE AdventureWorks
TO DISK = 'D:\Backups\AdventureWorks_Compressed.bak'
WITH COMPRESSION;

-- Xem compression ratio
SELECT 
    backup_size,
    compressed_backup_size,
    CAST(100 * (1 - compressed_backup_size * 1.0 / backup_size) AS DECIMAL(5,2)) 
        AS compression_savings_pct
FROM msdb.dbo.backupset
WHERE database_name = 'AdventureWorks'
ORDER BY backup_start_date DESC;

Backup Encryption (SQL Server 2014+)

-- Bước 1: Tạo master key
USE master;
CREATE MASTER KEY ENCRYPTION BY PASSWORD = 'StrongP@ssword123!';

-- Bước 2: Tạo certificate
CREATE CERTIFICATE BackupEncryptCert
WITH SUBJECT = 'SQL Server Backup Encryption Certificate';

-- Bước 3: Backup certificate (RẤT QUAN TRỌNG - mất cert = mất backup!)
BACKUP CERTIFICATE BackupEncryptCert
TO FILE = 'D:\Certs\BackupEncryptCert.cer'
WITH PRIVATE KEY (
    FILE = 'D:\Certs\BackupEncryptCert.pvk',
    ENCRYPTION BY PASSWORD = 'CertP@ssword123!'
);

-- Bước 4: Backup với encryption
BACKUP DATABASE AdventureWorks
TO DISK = 'D:\Backups\AdventureWorks_Encrypted.bak'
WITH 
    COMPRESSION,
    ENCRYPTION (
        ALGORITHM = AES_256,
        SERVER CERTIFICATE = BackupEncryptCert
    );

Database Snapshots

Database snapshot là read-only, point-in-time copy của database.

  • Cơ chế: copy-on-write — chỉ copy trang khi trang gốc bị thay đổi
  • Không thay thế backup — snapshot phụ thuộc vào source database
  • Dùng để: nhanh chóng revert về trạng thái trước, reporting, testing
-- Tạo database snapshot
CREATE DATABASE AdventureWorks_Snapshot_20260401
ON (
    NAME = 'AdventureWorks_Data',
    FILENAME = 'D:\Snapshots\AW_Snap_20260401.ss'
)
AS SNAPSHOT OF AdventureWorks;

-- Query từ snapshot
SELECT TOP 10 * 
FROM AdventureWorks_Snapshot_20260401.Sales.Orders;

-- Revert database về snapshot (database offline với users trong quá trình này)
RESTORE DATABASE AdventureWorks
FROM DATABASE_SNAPSHOT = 'AdventureWorks_Snapshot_20260401';

-- Xóa snapshot
DROP DATABASE AdventureWorks_Snapshot_20260401;

-- Xem tất cả snapshots
SELECT name, source_database_id, create_date
FROM sys.databases
WHERE source_database_id IS NOT NULL;

SQL Server Agent Backup Jobs

-- Xem các backup jobs trong SQL Agent
SELECT 
    j.name AS job_name,
    j.enabled,
    js.step_name,
    js.command,
    jsch.freq_type,
    jsch.freq_interval
FROM msdb.dbo.sysjobs j
JOIN msdb.dbo.sysjobsteps js ON j.job_id = js.job_id
JOIN msdb.dbo.sysjobschedules jsch ON j.job_id = jsch.job_id
WHERE js.command LIKE '%BACKUP%'
ORDER BY j.name;

-- Lịch sử các backup jobs gần nhất
SELECT TOP 20
    j.name AS job_name,
    jh.run_date,
    jh.run_time,
    CASE jh.run_status 
        WHEN 0 THEN 'Failed' 
        WHEN 1 THEN 'Succeeded' 
        WHEN 2 THEN 'Retry'
        WHEN 3 THEN 'Cancelled' 
    END AS run_status,
    jh.message
FROM msdb.dbo.sysjobs j
JOIN msdb.dbo.sysjobhistory jh ON j.job_id = jh.job_id
WHERE j.name LIKE '%Backup%'
ORDER BY jh.run_date DESC, jh.run_time DESC;

Ola Hallengren Backup Solution

Script backup phổ biến nhất trong cộng đồng SQL Server (thay thế cho custom scripts):

  • Download: https://ola.hallengren.com/
  • Hỗ trợ: full, differential, log backup; cleanup; verify; compression; encryption
  • Tự động skip databases đang OFFLINE hoặc RESTORING
-- Ví dụ sử dụng Ola Hallengren stored procedures
EXEC dbo.DatabaseBackup
    @Databases = 'AdventureWorks',
    @Directory = 'D:\Backups',
    @BackupType = 'FULL',
    @Compress = 'Y',
    @Verify = 'Y',
    @CheckSum = 'Y',
    @CleanupTime = 168;  -- Xóa backups cũ hơn 168 giờ (7 ngày)

EXEC dbo.DatabaseBackup
    @Databases = 'USER_DATABASES',
    @Directory = 'D:\Backups',
    @BackupType = 'LOG',
    @Compress = 'Y',
    @CleanupTime = 48;  -- Giữ log backups 48 giờ

Q&A - Phỏng vấn Backup & Recovery

Junior Level

Q1: Recovery model là gì và có bao nhiêu loại?

Recovery model quyết định cách SQL Server quản lý transaction log và loại backup nào được hỗ trợ. Có 3 loại:

  • SIMPLE: log tự truncate, không backup log được, RPO = thời gian giữa 2 full backup
  • FULL: log phải backup thủ công, hỗ trợ point-in-time recovery
  • BULK-LOGGED: giảm log cho bulk ops (BULK INSERT, index rebuild), vẫn cần backup log

Q2: Sự khác biệt giữa Full, Differential và Log backup?

  • Full: backup toàn bộ database — basis cho mọi restore chain
  • Differential: backup những gì thay đổi kể từ full backup cuối — nhỏ hơn full, restore nhanh hơn chuỗi log dài
  • Log: backup transaction log — cho phép point-in-time recovery, cần backup thường xuyên nhất

Q3: Khi nào dùng WITH NORECOVERY khi restore?

Khi còn cần apply thêm backup (differential hoặc log) sau đó. Database sẽ ở trạng thái RESTORING và không nhận connections. Backup cuối cùng trong chuỗi restore dùng WITH RECOVERY để bring database online.

Q4: Copy-only backup khác gì với full backup thông thường?

Copy-only backup không ảnh hưởng đến backup chain:

  • Full copy-only không reset differential base → differential backup sau vẫn dựa trên full backup trước đó
  • Log copy-only không truncate transaction log
  • Dùng khi cần backup ad-hoc mà không muốn can thiệp vào backup schedule chính

Mid Level

Q5: Giải thích quy trình restore point-in-time. Các bước thực hiện?

  1. Tail-log backup: backup phần log chưa backup (dùng WITH NORECOVERY để database vào RESTORING)
  2. Restore full backup: RESTORE DATABASE ... FROM DISK ... WITH NORECOVERY
  3. Restore differential backup (nếu có): RESTORE DATABASE ... WITH NORECOVERY
  4. Restore log backups tuần tự: mỗi file log WITH NORECOVERY, log cuối cùng WITH RECOVERY, STOPAT = 'datetime'

Q6: RPO và RTO là gì? Làm thế nào để đạt RPO = 15 phút?

  • RPO (Recovery Point Objective): tối đa mất bao nhiêu dữ liệu (đo bằng thời gian)
  • RTO (Recovery Time Objective): phục hồi trong bao lâu (downtime tối đa)

Để đạt RPO = 15 phút: chạy log backup mỗi 15 phút trên database ở FULL recovery model. Trong trường hợp disaster, mất tối đa 15 phút dữ liệu.

Q7: Tail-log backup là gì và khi nào cần thiết?

Tail-log backup là log backup của phần transaction log chưa được backup — thực hiện ngay trước khi restore để không mất dữ liệu từ lần log backup cuối đến thời điểm sự cố.

Cần thiết khi:

  • Database bị corrupt hoặc mất do hardware failure nhưng log file còn intact
  • Dùng WITH NO_TRUNCATE nếu data files bị hỏng

Q8: Database snapshot hoạt động như thế nào? Có thể dùng thay backup không?

Snapshot dùng cơ chế copy-on-write: khi trang gốc lần đầu bị sửa đổi sau khi tạo snapshot, trang gốc được copy vào file snapshot trước khi ghi dữ liệu mới.

Không thể thay thế backup vì:

  • Snapshot lưu trên cùng server, cùng disk → không protect khỏi hardware failure
  • Snapshot phụ thuộc hoàn toàn vào source database — source bị hỏng, snapshot cũng mất
  • Snapshot không cover server outage, disk failure

Senior Level

Q9: Thiết kế backup strategy cho OLTP database 2TB, RPO = 30 phút, RTO = 2 giờ, ngân sách backup storage hạn chế?

Phân tích: 2TB full backup sẽ lớn và tốn thời gian/storage. Cần tối ưu:

Strategy:

  • Full backup: 1 lần/tuần (Chủ Nhật 00:00) — dùng WITH COMPRESSION (typically giảm 50-70%)
  • Differential backup: hàng đêm — tăng trưởng theo tỷ lệ thay đổi, không theo kích thước database
  • Log backup: mỗi 30 phút — đảm bảo RPO
  • Retention: Full: 4 tuần, Differential: 2 tuần, Log: 72 giờ
  • Backup destination: local disk + cloud (Azure Blob) với lifecycle policy

RTO 2 giờ: với 2TB, restore từ full + differential + logs có thể mất hơn 2 giờ. Cân nhắc:

  • Filegroup backup strategy nếu chỉ một phần crash
  • Always On AG với async secondary để failover nhanh hơn

Q10: Giải thích backup chain và khi nào chain bị broken?

Backup chain là chuỗi liên tục từ full backup → (optional differential) → sequential log backups. Chain đảm bảo restore được đến bất kỳ thời điểm nào trong khoảng covered.

Chain bị broken khi:

  1. Switch từ FULL sang SIMPLE rồi back sang FULL → chain reset, phải lấy new full backup
  2. Log backup bị thiếu/miss → không restore qua khoảng đó được
  3. Database detach/attach không đúng cách
  4. TRUNCATE_ONLY (SQL 2008 trở về trước, đã deprecated)

Cách kiểm tra: query msdb.dbo.backupset và verify không có gap trong sequence của log backups.

Q11: Khi nào dùng page-level restore? Ưu điểm so với full restore?

Page-level restore khi:

  • Chỉ một vài trang (pages) bị corrupt, phần còn lại database hoạt động tốt
  • Muốn giảm thiểu downtime — database vẫn ONLINE, chỉ các trang bị corrupt không accessible
  • Phát hiện qua DBCC CHECKDB hoặc msdb.dbo.suspect_pages

Ưu điểm: không cần offline cả database → RTO gần như zero cho phần còn lại. Sau khi restore trang và apply log, database hoàn toàn normal.

Điều kiện: FULL recovery model, Enterprise Edition (hoặc Developer)

SQL Server Agent & Jobs

SQL Server Agent Overview

SQL Server Agent là Windows Service (SQLAGENT.EXE) chịu trách nhiệm automation trong SQL Server: chạy scheduled jobs, phản hồi alerts, gửi notifications. Là backbone của mọi tác vụ tự động hóa trong SQL Server.

-- Kiểm tra SQL Agent service status
EXEC master.dbo.xp_servicecontrol 'QUERYSTATE', 'SQLServerAgent';

-- Hoặc dùng sys.dm_server_services
SELECT servicename, status_desc, startup_type_desc, last_startup_time
FROM sys.dm_server_services
WHERE servicename LIKE 'SQL Server Agent%';

Thành phần của SQL Server Agent

Thành phầnMô tả
JobsTập hợp các steps được thực thi theo schedule hoặc on-demand
SchedulesĐịnh nghĩa khi nào jobs chạy
AlertsPhản hồi tự động khi có event/condition
OperatorsNgười nhận thông báo (email, pager)
ProxiesCredential context cho job steps

Jobs

Tạo Job bằng T-SQL

-- Bước 1: Tạo Job
USE msdb;
EXEC sp_add_job
    @job_name        = N'Daily_Index_Maintenance',
    @enabled         = 1,
    @description     = N'Daily index rebuild and reorganize for all user tables',
    @category_name   = N'Database Maintenance',
    @owner_login_name = N'sa';

-- Bước 2: Thêm Job Step
EXEC sp_add_jobstep
    @job_name         = N'Daily_Index_Maintenance',
    @step_name        = N'Rebuild Fragmented Indexes',
    @step_id          = 1,
    @subsystem        = N'TSQL',  -- T-SQL step
    @command          = N'
EXEC master.sys.sp_MSforeachdb ''
    IF DATABASEPROPERTYEX(''''?'''', ''''Status'''') = ''''ONLINE''''
    AND ''''?'''' NOT IN (''''master'''', ''''model'''', ''''msdb'''', ''''tempdb'''')
    BEGIN
        USE [?];
        EXEC dbo.IndexMaintenance;
    END
'';',
    @database_name    = N'master',
    @on_success_action = 1,  -- 1=Quit with success, 2=Quit with failure, 3=Go to next step, 4=Go to step
    @on_fail_action    = 2,  -- Quit with failure
    @retry_attempts    = 2,
    @retry_interval    = 5;  -- Minutes

-- Bước 3: Thêm Schedule
EXEC sp_add_schedule
    @schedule_name        = N'Daily_2AM',
    @freq_type            = 4,          -- 4=Daily, 8=Weekly, 16=Monthly
    @freq_interval        = 1,          -- Every 1 day
    @active_start_time    = 20000,      -- 02:00:00 (HHMMSS format)
    @active_end_time      = 235959,     -- 23:59:59
    @active_start_date    = 20240101;   -- YYYYMMDD

-- Bước 4: Gán Schedule vào Job
EXEC sp_attach_schedule
    @job_name     = N'Daily_Index_Maintenance',
    @schedule_name = N'Daily_2AM';

-- Bước 5: Đăng ký job với server
EXEC sp_add_jobserver
    @job_name   = N'Daily_Index_Maintenance',
    @server_name = N'(local)';

Xóa Job

EXEC sp_delete_job @job_name = N'Daily_Index_Maintenance';
-- Hoặc
EXEC sp_delete_job @job_id = '12345678-1234-1234-1234-123456789012';

Job Steps và Subsystems

Các loại Job Step (Subsystems)

SubsystemMô tảYêu cầu
TSQLChạy T-SQL script trong SQL ServerDatabase name
SSISChạy SQL Server Integration Services packageSSIS catalog hoặc file
PowerShellChạy PowerShell scriptPowerShell 2.0+
CmdExecChạy Windows command (exe, bat, cmd)OS command access
ActiveScriptingVBScript/JScript (legacy, deprecated)
LogReaderInternal: Log Reader Agent cho replication
DistributionInternal: Distribution Agent
-- PowerShell Job Step
EXEC sp_add_jobstep
    @job_name    = N'Deploy_SSIS_Package',
    @step_name   = N'Run PowerShell Deployment',
    @subsystem   = N'PowerShell',
    @command     = N'
$filePath = "C:\SSIS\MyPackage.ispac"
$catalog = "SSISDB"
# ... PowerShell deployment script
',
    @on_success_action = 1,
    @on_fail_action    = 2;

-- CmdExec Job Step (dùng với Proxy để tránh chạy với SQL Agent account)
EXEC sp_add_jobstep
    @job_name      = N'Backup_Compress',
    @step_name     = N'Compress Backup Files',
    @subsystem     = N'CmdExec',
    @command       = N'"C:\7zip\7z.exe" a -t7z "D:\Backups\backup.7z" "D:\Backups\*.bak"',
    @proxy_name    = N'BackupProxy',  -- Chạy dưới identity của proxy
    @on_success_action = 1;

Schedules

Các loại Schedule

-- One-time (chạy một lần)
EXEC sp_add_schedule
    @schedule_name     = N'One_Time_Migration',
    @freq_type         = 1,          -- 1=Once
    @active_start_date = 20250401,   -- Run date
    @active_start_time = 020000;     -- 02:00:00

-- Daily (mỗi ngày)
EXEC sp_add_schedule
    @schedule_name     = N'Every_Day_3AM',
    @freq_type         = 4,          -- 4=Daily
    @freq_interval     = 1,          -- Every 1 day
    @active_start_time = 030000;

-- Weekly (mỗi tuần - ví dụ: thứ 2, thứ 4, thứ 6)
EXEC sp_add_schedule
    @schedule_name     = N'MWF_6AM',
    @freq_type         = 8,          -- 8=Weekly
    @freq_interval     = 42,         -- Bit mask: Mon=2, Wed=8, Fri=32 → 2+8+32=42
    @freq_recurrence_factor = 1,     -- Every 1 week
    @active_start_time = 060000;

-- Monthly (ngày đầu mỗi tháng)
EXEC sp_add_schedule
    @schedule_name     = N'Monthly_First_Day',
    @freq_type         = 16,         -- 16=Monthly
    @freq_interval     = 1,          -- Day 1 of month
    @freq_recurrence_factor = 1,     -- Every 1 month
    @active_start_time = 010000;

-- Recurring intraday (mỗi 15 phút từ 8AM đến 10PM)
EXEC sp_add_schedule
    @schedule_name          = N'Every_15min_8AM_to_10PM',
    @freq_type              = 4,          -- Daily
    @freq_interval          = 1,
    @freq_subday_type       = 4,          -- 4=Minutes
    @freq_subday_interval   = 15,         -- Every 15 minutes
    @active_start_time      = 080000,     -- 08:00:00
    @active_end_time        = 220000;     -- 22:00:00

freq_interval Bitmask cho Weekly Schedule

DayBit Value
Sunday1
Monday2
Tuesday4
Wednesday8
Thursday16
Friday32
Saturday64

Alerts

Performance Condition Alert

-- Alert khi CPU vượt 90%
EXEC sp_add_alert
    @name                       = N'High CPU Usage',
    @alert_type                 = 2,           -- 2=SQL Server performance condition
    @performance_condition      = N'SQLServer:Resource Pool Stats|CPU usage %|default|>|90',
    @job_name                   = N'Investigate_High_CPU',  -- Job để chạy khi alert trigger
    @notification_message       = N'CPU usage exceeded 90%! Investigate immediately.';

-- Alert khi error number cụ thể
EXEC sp_add_alert
    @name              = N'Severity 19-25 Errors',
    @alert_type        = 1,          -- 1=SQL Server event
    @severity          = 19,         -- Severity level (1-25), NULL để dùng message_id
    @notification_message = N'Fatal SQL Server error occurred',
    @job_name          = N'Capture_Error_Details';

-- Alert cho một error number cụ thể
EXEC sp_add_alert
    @name              = N'Deadlock Detected',
    @alert_type        = 1,
    @message_id        = 1205,       -- Deadlock error number
    @notification_message = N'Deadlock occurred';

-- WMI Event Alert
EXEC sp_add_alert
    @name              = N'Low Disk Space',
    @alert_type        = 3,          -- 3=WMI event
    @wmi_namespace     = N'\\.\root\cimv2',
    @wmi_query         = N'SELECT * FROM __InstanceModificationEvent WITHIN 60 WHERE TargetInstance ISA ''Win32_LogicalDisk'' AND TargetInstance.DriveType = 3 AND TargetInstance.FreeSpace < 1073741824';

Operators

Operators là người nhận notification khi job fail hoặc alert trigger.

-- Tạo Operator
EXEC sp_add_operator
    @name                  = N'DBA Team',
    @enabled               = 1,
    @email_address         = N'dba-team@company.com',
    @weekday_pager_start_time = 090000,  -- Pager hours (legacy)
    @weekday_pager_end_time   = 180000;

-- Gửi notification khi Job fail/succeed
EXEC sp_add_notification
    @alert_name   = N'Deadlock Detected',
    @operator_name = N'DBA Team',
    @notification_method = 1;  -- 1=Email, 2=Pager, 4=Net Send (legacy)

-- Cấu hình notification cho job
EXEC sp_update_job
    @job_name          = N'Daily_Index_Maintenance',
    @notify_level_email  = 2,          -- 1=Success, 2=Failure, 3=Always
    @notify_email_operator_name = N'DBA Team',
    @notify_level_page   = 0,
    @notify_level_netsend = 0;

Database Mail

Cấu hình Database Mail

-- Bước 1: Enable Database Mail XPs
EXEC sp_configure 'show advanced options', 1;
RECONFIGURE;
EXEC sp_configure 'Database Mail XPs', 1;
RECONFIGURE;

-- Bước 2: Tạo Mail Account
EXEC msdb.dbo.sysmail_add_account_sp
    @account_name            = N'SQLServer_DBMail',
    @description             = N'SQL Server Database Mail Account',
    @email_address           = N'sqlserver@company.com',
    @display_name            = N'SQL Server Notifications',
    @replyto_address         = N'no-reply@company.com',
    @mailserver_name         = N'smtp.company.com',
    @mailserver_type         = N'SMTP',
    @port                    = 587,
    @username                = N'sqlserver@company.com',
    @password                = N'YourPassword',  -- Lưu an toàn trong credential
    @use_default_credentials = 0,
    @enable_ssl              = 1;

-- Bước 3: Tạo Mail Profile
EXEC msdb.dbo.sysmail_add_profile_sp
    @profile_name = N'DBA_Alerts',
    @description  = N'Profile for DBA alert notifications';

-- Bước 4: Gán Account vào Profile
EXEC msdb.dbo.sysmail_add_profileaccount_sp
    @profile_name  = N'DBA_Alerts',
    @account_name  = N'SQLServer_DBMail',
    @sequence_number = 1;

-- Bước 5: Grant public access (hoặc chỉ cho các principals cụ thể)
EXEC msdb.dbo.sysmail_add_principalprofile_sp
    @profile_name      = N'DBA_Alerts',
    @principal_name    = N'public',
    @is_default        = 1;

-- Test gửi email
EXEC msdb.dbo.sp_send_dbmail
    @profile_name  = N'DBA_Alerts',
    @recipients    = N'dba-team@company.com',
    @subject       = N'Test Database Mail',
    @body          = N'Database Mail is configured and working.';

Monitoring Jobs

Xem Job Status và History

-- Xem tất cả jobs và trạng thái
SELECT 
    j.name AS job_name,
    j.enabled,
    j.description,
    CASE j.last_run_outcome
        WHEN 0 THEN 'Failed'
        WHEN 1 THEN 'Succeeded'
        WHEN 3 THEN 'Canceled'
        WHEN 5 THEN 'Unknown'
    END AS last_run_outcome,
    j.last_run_date,
    j.last_run_time,
    j.next_run_date,
    j.next_run_time
FROM msdb.dbo.sysjobs j
LEFT JOIN msdb.dbo.sysjobservers js ON j.job_id = js.job_id
ORDER BY j.name;

-- Xem job history chi tiết
SELECT TOP 100
    j.name AS job_name,
    jh.step_name,
    jh.step_id,
    CASE jh.run_status
        WHEN 0 THEN 'Failed'
        WHEN 1 THEN 'Succeeded'
        WHEN 2 THEN 'Retry'
        WHEN 3 THEN 'Canceled'
    END AS run_status,
    -- Convert run_date (int) và run_time (int) sang datetime
    CONVERT(DATETIME, 
        STUFF(STUFF(CAST(jh.run_date AS VARCHAR(8)), 5, 0, '-'), 8, 0, '-') + ' ' +
        STUFF(STUFF(RIGHT('000000' + CAST(jh.run_time AS VARCHAR(6)), 6), 3, 0, ':'), 6, 0, ':')
    ) AS run_datetime,
    jh.run_duration,  -- Format: HHMMSS
    jh.message
FROM msdb.dbo.sysjobhistory jh
JOIN msdb.dbo.sysjobs j ON jh.job_id = j.job_id
WHERE jh.run_status = 0  -- Chỉ failed
ORDER BY jh.instance_id DESC;

-- Xem jobs đang chạy hiện tại
SELECT 
    j.name AS job_name,
    ja.start_execution_date,
    DATEDIFF(MINUTE, ja.start_execution_date, GETDATE()) AS running_minutes,
    ja.last_executed_step_id,
    s.step_name AS current_step
FROM msdb.dbo.sysjobactivity ja
JOIN msdb.dbo.sysjobs j ON ja.job_id = j.job_id
JOIN msdb.dbo.sysjobsteps s ON ja.job_id = s.job_id 
    AND ja.last_executed_step_id = s.step_id
WHERE ja.session_id = (SELECT MAX(session_id) FROM msdb.dbo.syssessions)
    AND ja.start_execution_date IS NOT NULL
    AND ja.stop_execution_date IS NULL
ORDER BY ja.start_execution_date;

Job Categories

-- Tạo Job Category
EXEC sp_add_category
    @class    = N'JOB',
    @type     = N'LOCAL',
    @name     = N'Database Maintenance';

EXEC sp_add_category @class = N'JOB', @type = N'LOCAL', @name = N'ETL Jobs';
EXEC sp_add_category @class = N'JOB', @type = N'LOCAL', @name = N'Monitoring';

-- Xem tất cả categories
SELECT name, category_id, category_type
FROM msdb.dbo.syscategories
WHERE category_class = 1  -- 1=JOB
ORDER BY name;

-- Gán job vào category
EXEC sp_update_job
    @job_name      = N'Daily_Index_Maintenance',
    @category_name = N'Database Maintenance';

Proxy Accounts

Proxy cho phép Job Steps chạy với identity khác (không phải SQL Agent service account), cần cho CmdExec, PowerShell, SSIS steps.

-- Bước 1: Tạo Windows credential
CREATE CREDENTIAL [BackupServiceCredential]
WITH IDENTITY = N'DOMAIN\svc_backup',
SECRET = N'StrongP@ssword';

-- Bước 2: Tạo Proxy dùng credential
EXEC msdb.dbo.sp_add_proxy
    @proxy_name          = N'BackupProxy',
    @credential_name     = N'BackupServiceCredential',
    @enabled             = 1,
    @description         = N'Proxy for backup service account';

-- Bước 3: Grant proxy access cho subsystems
EXEC msdb.dbo.sp_grant_proxy_to_subsystem
    @proxy_name    = N'BackupProxy',
    @subsystem_id  = 3;   -- 3=CmdExec

-- Subsystem IDs: 2=ActiveScripting, 3=CmdExec, 9=SSIS, 12=PowerShell

-- Bước 4: Grant logins quyền dùng proxy
EXEC msdb.dbo.sp_grant_login_to_proxy
    @login_name  = N'JobOwnerLogin',
    @proxy_name  = N'BackupProxy';

-- Xem proxies
SELECT p.name, p.enabled, c.name AS credential_name
FROM msdb.dbo.sysproxies p
JOIN sys.credentials c ON p.credential_id = c.credential_id;

Multi-Server Administration (MSX/TSX)

-- Master Server (MSX) setup
-- Tư duy: MSX quản lý multiple Target Servers (TSX)
-- Jobs tạo trên MSX sẽ được download và chạy trên các TSX

-- Enlist một server làm MSX
EXEC sp_msx_enlist @msx_server_name = N'MASTER_SERVER';

-- Tạo multi-server job
EXEC sp_add_job
    @job_name = N'All_Servers_Backup',
    @enabled  = 1;

EXEC sp_add_jobstep @job_name = N'All_Servers_Backup', ...;

-- Target specific servers hoặc tất cả
EXEC sp_add_jobserver
    @job_name   = N'All_Servers_Backup',
    @server_name = N'ALL';  -- Chạy trên tất cả target servers
    
-- Hoặc chỉ một server cụ thể
EXEC sp_add_jobserver
    @job_name   = N'All_Servers_Backup',
    @server_name = N'TARGET_SERVER_1';

Maintenance Plans và Best Practices

Ola Hallengren’s SQL Server Maintenance Solution

-- Ola Hallengren solution là best practice cho maintenance
-- Download từ: https://ola.hallengren.com/

-- Sau khi install, tạo jobs dùng stored procedures:

-- Index Maintenance (rebuild/reorganize based on fragmentation)
EXEC dbo.IndexOptimize
    @Databases        = 'USER_DATABASES',
    @FragmentationLow = NULL,           -- Không làm gì khi ít fragmentation
    @FragmentationMedium = 'INDEX_REORGANIZE',   -- Reorganize 5-30%
    @FragmentationHigh = 'INDEX_REBUILD_ONLINE,INDEX_REBUILD_OFFLINE',  -- Rebuild >30%
    @FragmentationLevel1 = 5,
    @FragmentationLevel2 = 30,
    @MinNumberOfPages = 1000,           -- Bỏ qua indexes nhỏ
    @SortInTempdb = 'Y',
    @MaxDOP = 4;

-- Statistics Update
EXEC dbo.IndexOptimize
    @Databases    = 'USER_DATABASES',
    @Indexes      = NULL,
    @UpdateStatistics = 'ALL',
    @OnlyModifiedStatistics = 'Y';

-- Database Integrity Check
EXEC dbo.DatabaseIntegrityCheck
    @Databases = 'USER_DATABASES',
    @CheckCommands = 'CHECKDB';

-- Database Backup
EXEC dbo.DatabaseBackup
    @Databases   = 'USER_DATABASES',
    @Directory   = 'D:\Backups',
    @BackupType  = 'FULL',
    @Verify      = 'Y',
    @Compress    = 'Y',
    @CleanupTime = 48;  -- Xóa backup cũ hơn 48 giờ

Common Automated Tasks

Backup Job

-- Full backup job step
EXEC sp_add_jobstep
    @job_name    = N'Full_Database_Backup',
    @step_name   = N'Backup User Databases',
    @subsystem   = N'TSQL',
    @command     = N'
DECLARE @BackupPath NVARCHAR(500) = N''D:\Backups\'';
DECLARE @FileName NVARCHAR(500);
DECLARE @DBName NVARCHAR(128);

DECLARE db_cursor CURSOR FOR
SELECT name FROM sys.databases
WHERE state_desc = ''ONLINE''
    AND name NOT IN (''tempdb'')
    AND is_read_only = 0;

OPEN db_cursor;
FETCH NEXT FROM db_cursor INTO @DBName;

WHILE @@FETCH_STATUS = 0
BEGIN
    SET @FileName = @BackupPath + @DBName + ''_FULL_'' + 
        REPLACE(REPLACE(CONVERT(VARCHAR, GETDATE(), 120), '':'', ''''), '' '', ''_'') + ''.bak'';
    
    BACKUP DATABASE @DBName 
    TO DISK = @FileName
    WITH COMPRESSION, CHECKSUM, STATS = 10;
    
    FETCH NEXT FROM db_cursor INTO @DBName;
END;

CLOSE db_cursor;
DEALLOCATE db_cursor;
',
    @database_name = N'master';

DBCC CHECKDB Job

-- Chạy DBCC CHECKDB hàng tuần cuối tuần
EXEC sp_add_jobstep
    @job_name    = N'Weekly_Integrity_Check',
    @step_name   = N'DBCC CHECKDB All Databases',
    @subsystem   = N'TSQL',
    @command     = N'
EXEC sp_MSforeachdb ''
IF DATABASEPROPERTYEX(''''?'''', ''''Status'''') = ''''ONLINE''''
AND ''''?'''' NOT IN (''''tempdb'''')
BEGIN
    DBCC CHECKDB (''''?'''') WITH NO_INFOMSGS, ALL_ERRORMSGS, DATA_PURITY;
END
'';';

Log Shipping Restore Job

-- Restore transaction log backup (phần của Log Shipping)
EXEC sp_add_jobstep
    @job_name    = N'LogShipping_Restore_YourDB',
    @step_name   = N'Restore Latest Log Backup',
    @subsystem   = N'TSQL',
    @command     = N'
DECLARE @LatestFile NVARCHAR(500);
SELECT TOP 1 @LatestFile = physical_device_name
FROM msdb.dbo.backupset bs
JOIN msdb.dbo.backupmediafamily bmf ON bs.media_set_id = bmf.media_set_id
WHERE bs.database_name = N''SourceDB''
    AND bs.type = N''L''
    AND bs.backup_finish_date > GETDATE() - 1
ORDER BY bs.backup_finish_date DESC;

IF @LatestFile IS NOT NULL
BEGIN
    RESTORE LOG YourDB
    FROM DISK = @LatestFile
    WITH STANDBY = N''D:\Standby\YourDB_standby.bak'', STATS = 10;
END;';

Q&A theo Cấp Độ

Junior Level

Q: SQL Server Agent là gì và tại sao quan trọng?

A: SQL Server Agent là Windows service tự động hóa các tác vụ trong SQL Server: chạy scheduled jobs (backup, maintenance, ETL), phản hồi alerts (disk space, high CPU, errors), gửi notifications qua Database Mail. Thiếu SQL Agent thì không thể có CDC capture jobs, log shipping, replication agents, database mail.

Q: Làm sao xem lịch sử chạy của một job và biết nó fail vì sao?

A:

-- Qua SSMS: SQL Server Agent → Jobs → Right-click → View History
-- Qua T-SQL:
SELECT TOP 50
    j.name, jh.step_name, 
    CASE jh.run_status WHEN 0 THEN 'Failed' WHEN 1 THEN 'Success' END AS status,
    jh.message
FROM msdb.dbo.sysjobhistory jh
JOIN msdb.dbo.sysjobs j ON jh.job_id = j.job_id
WHERE j.name = 'YourJobName'
ORDER BY jh.instance_id DESC;

Q: Database Mail là gì và cần cấu hình gì để gửi được email?

A: Database Mail là component của SQL Server cho phép gửi email từ T-SQL (sp_send_dbmail). Cần cấu hình:

  1. Enable Database Mail XPs qua sp_configure
  2. Tạo Mail Account (SMTP server, port, credentials)
  3. Tạo Mail Profile và gán Account vào Profile
  4. Grant principals quyền dùng profile Test bằng EXEC msdb.dbo.sp_send_dbmail.

Mid Level

Q: Proxy Account trong SQL Server Agent là gì và khi nào cần dùng?

A: Proxy là identity context (Windows account) mà một job step chạy dưới đó, thay vì chạy dưới SQL Agent service account. Cần dùng khi:

  • CmdExec hay PowerShell steps cần quyền Windows cụ thể
  • SSIS package cần truy cập file share, network resource với specific credential
  • Security principle of least privilege: không muốn SQL Agent service account có quá nhiều quyền

Cách tạo: CREATE CREDENTIALsp_add_proxysp_grant_proxy_to_subsystemsp_grant_login_to_proxy.

Q: Giải thích các on_success_actionon_fail_action options trong job steps?

A: Mỗi step có 2 flow control actions:

  • 1 = Quit with success (kết thúc job thành công)
  • 2 = Quit with failure (kết thúc job thất bại)
  • 3 = Go to next step (tiếp tục step tiếp theo)
  • 4 = Go to step N (nhảy đến step cụ thể - dùng cho conditional logic)

Pattern phổ biến: Step 1 → success: Go to next, fail: Go to Step 5 (step cleanup/notification). Step 5 → always quit với failure. Cho phép tạo “workflow” phức tạp.


Senior Level

Q: Làm sao thiết kế một robust job framework với alerting, retry logic, và audit trail?

A: Framework đầy đủ cần:

1. Control table để track job execution:

CREATE TABLE dbo.JobExecutionLog (
    LogID         BIGINT IDENTITY PRIMARY KEY,
    JobName       NVARCHAR(128),
    StepName      NVARCHAR(128),
    StartTime     DATETIME2 DEFAULT SYSUTCDATETIME(),
    EndTime       DATETIME2,
    Status        VARCHAR(20),  -- Running, Success, Failed, Retrying
    ErrorMessage  NVARCHAR(MAX),
    RetryCount    INT DEFAULT 0,
    RowsProcessed BIGINT
);

2. Wrapper procedure với retry logic:

CREATE OR ALTER PROCEDURE dbo.usp_JobStepWrapper
    @ProcName  NVARCHAR(200),
    @MaxRetries INT = 3,
    @RetryDelaySeconds INT = 60
AS BEGIN
    DECLARE @Attempt INT = 0, @LogID BIGINT;
    
    INSERT INTO dbo.JobExecutionLog (JobName, Status) 
    VALUES (@ProcName, 'Running');
    SET @LogID = SCOPE_IDENTITY();
    
    WHILE @Attempt < @MaxRetries
    BEGIN
        SET @Attempt += 1;
        BEGIN TRY
            EXEC sp_executesql @ProcName;
            UPDATE dbo.JobExecutionLog 
            SET Status = 'Success', EndTime = SYSUTCDATETIME() 
            WHERE LogID = @LogID;
            RETURN;
        END TRY
        BEGIN CATCH
            UPDATE dbo.JobExecutionLog 
            SET Status = 'Retrying', ErrorMessage = ERROR_MESSAGE(), RetryCount = @Attempt
            WHERE LogID = @LogID;
            
            IF @Attempt < @MaxRetries
                WAITFOR DELAY @RetryDelaySeconds;
        END CATCH
    END
    
    -- All retries exhausted
    UPDATE dbo.JobExecutionLog SET Status = 'Failed' WHERE LogID = @LogID;
    EXEC msdb.dbo.sp_send_dbmail
        @profile_name = 'DBA_Alerts',
        @recipients = 'dba@company.com',
        @subject = 'Job Failed: ' + @ProcName,
        @body = 'All retries exhausted.';
    RAISERROR('Job failed after all retries', 16, 1);
END;

3. Monitoring query:

-- Jobs chạy quá lâu, failed gần đây, hoặc chưa chạy đúng schedule
SELECT j.name, ja.start_execution_date, 
       DATEDIFF(MINUTE, ja.start_execution_date, GETDATE()) AS running_min
FROM msdb.dbo.sysjobactivity ja
JOIN msdb.dbo.sysjobs j ON ja.job_id = j.job_id
WHERE ja.stop_execution_date IS NULL
    AND DATEDIFF(MINUTE, ja.start_execution_date, GETDATE()) > 60  -- Running > 1 hour
ORDER BY running_min DESC;

Q: Giải thích cách implement job dependencies (Job A phải chạy xong trước Job B)?

A: SQL Server Agent không có built-in job dependency mechanism. Các cách implement:

Option 1: Linked steps trong cùng job: Đặt tất cả logic vào một job với nhiều steps — đơn giản nhất.

Option 2: Check job completion trong step:

-- Ở đầu Job B, kiểm tra Job A đã complete hôm nay chưa
DECLARE @JobAStatus INT;
SELECT TOP 1 @JobAStatus = run_status
FROM msdb.dbo.sysjobhistory jh
JOIN msdb.dbo.sysjobs j ON jh.job_id = j.job_id
WHERE j.name = 'Job_A'
    AND run_status = 1  -- Success
    AND run_date = CONVERT(INT, CONVERT(VARCHAR, GETDATE(), 112))
ORDER BY instance_id DESC;

IF @JobAStatus != 1
    RAISERROR('Job A has not completed successfully today.', 16, 1);

Option 3: SQL Server Agent Tokens + Custom table: Job A update dbo.JobStatus sau khi complete. Job B kiểm tra table này.

Option 4: SSIS Pipeline: Dùng SSIS như orchestration engine với built-in precedence constraints.

Option 5: External orchestration: Azure Data Factory, Apache Airflow, hoặc SQL Server 2019+ với external job scheduler.

Monitoring & Diagnostics

Dynamic Management Views (DMVs)

DMV là các views và functions hệ thống cung cấp thông tin runtime về SQL Server — không cần third-party tools.

Phân loại DMVs

  • sys.dm_exec_*: query execution, sessions, requests
  • sys.dm_os_*: operating system, memory, waits, schedulers
  • sys.dm_tran_*: transactions và locks
  • sys.dm_io_*: I/O
  • sys.dm_db_*: database-level metrics
  • sys.dm_hadr_*: Always On AG health

1. sys.dm_exec_requests — Queries đang chạy

-- Xem tất cả queries đang thực thi
SELECT 
    r.session_id,
    r.status,
    r.start_time,
    r.command,
    DATEDIFF(SECOND, r.start_time, GETDATE()) AS duration_seconds,
    r.cpu_time,
    r.reads,
    r.writes,
    r.logical_reads,
    r.wait_type,
    r.wait_time,
    r.blocking_session_id,
    DB_NAME(r.database_id) AS database_name,
    SUBSTRING(t.text, 
        (r.statement_start_offset / 2) + 1,
        CASE r.statement_end_offset 
            WHEN -1 THEN DATALENGTH(t.text)
            ELSE r.statement_end_offset 
        END - r.statement_start_offset) / 2 + 1) AS current_statement,
    t.text AS full_query,
    qp.query_plan
FROM sys.dm_exec_requests r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) qp
WHERE r.session_id > 50  -- Exclude system sessions
    AND r.status != 'background'
ORDER BY r.cpu_time DESC;

-- Tìm query đang bị block
SELECT 
    r.session_id,
    r.blocking_session_id,
    DATEDIFF(SECOND, r.start_time, GETDATE()) AS blocked_for_seconds,
    t.text AS blocked_query,
    r.wait_type
FROM sys.dm_exec_requests r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
WHERE r.blocking_session_id > 0;

2. sys.dm_exec_sessions — Connected Sessions

-- Xem tất cả sessions đang kết nối
SELECT 
    s.session_id,
    s.login_name,
    s.host_name,
    s.program_name,
    s.status,
    s.cpu_time,
    s.memory_usage * 8 AS memory_kb,
    s.total_elapsed_time / 1000 AS elapsed_seconds,
    s.last_request_start_time,
    s.reads,
    s.writes,
    s.logical_reads,
    DB_NAME(s.database_id) AS current_database,
    s.open_transaction_count
FROM sys.dm_exec_sessions s
WHERE s.is_user_process = 1  -- Chỉ user sessions, không phải system
ORDER BY s.cpu_time DESC;

-- Xem sessions với open transactions lâu (potential blocking)
SELECT 
    s.session_id,
    s.login_name,
    s.open_transaction_count,
    DATEDIFF(MINUTE, s.last_request_start_time, GETDATE()) AS idle_minutes,
    t.text AS last_query
FROM sys.dm_exec_sessions s
CROSS APPLY sys.dm_exec_sql_text(s.most_recent_sql_handle) t
WHERE s.open_transaction_count > 0
    AND s.is_user_process = 1
ORDER BY idle_minutes DESC;

3. sys.dm_exec_query_stats — Query Statistics từ Cache

-- Top 10 queries tiêu tốn CPU nhiều nhất
SELECT TOP 10
    qs.execution_count,
    qs.total_worker_time / 1000 AS total_cpu_ms,
    qs.total_worker_time / qs.execution_count / 1000 AS avg_cpu_ms,
    qs.total_logical_reads,
    qs.total_logical_reads / qs.execution_count AS avg_logical_reads,
    qs.total_elapsed_time / 1000 AS total_elapsed_ms,
    qs.total_elapsed_time / qs.execution_count / 1000 AS avg_elapsed_ms,
    qs.creation_time AS plan_created_time,
    SUBSTRING(t.text, 
        (qs.statement_start_offset / 2) + 1,
        (ISNULL(qs.statement_end_offset, DATALENGTH(t.text)) - 
         qs.statement_start_offset) / 2 + 1) AS query_text,
    qp.query_plan
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) t
CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) qp
ORDER BY qs.total_worker_time DESC;

-- Top 10 queries tốn I/O (logical reads)
SELECT TOP 10
    qs.total_logical_reads / qs.execution_count AS avg_logical_reads,
    qs.execution_count,
    qs.total_logical_reads,
    SUBSTRING(t.text, 1, 200) AS query_snippet
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) t
ORDER BY avg_logical_reads DESC;

4. sys.dm_os_wait_stats — Wait Statistics

Wait statistics là cách tốt nhất để chẩn đoán bottleneck.

-- Top wait types (loại trừ idle waits)
SELECT TOP 20
    wait_type,
    wait_time_ms / 1000.0 AS wait_time_seconds,
    waiting_tasks_count,
    wait_time_ms / NULLIF(waiting_tasks_count, 0) AS avg_wait_ms,
    CAST(wait_time_ms * 100.0 / SUM(wait_time_ms) OVER() AS DECIMAL(5,2)) AS pct_total
FROM sys.dm_os_wait_stats
WHERE wait_type NOT IN (
    -- Idle/benign waits - loại trừ
    'SLEEP_TASK', 'SLEEP_SYSTEMTASK', 'BROKER_TO_FLUSH',
    'BROKER_TASK_STOP', 'CLR_AUTO_EVENT', 'CLR_MANUAL_EVENT',
    'DISPATCHER_QUEUE_SEMAPHORE', 'FT_IFTS_SCHEDULER_IDLE_WAIT',
    'HADR_FILESTREAM_IOMGR_IOCOMPLETION', 'HADR_WORK_QUEUE',
    'LAZYWRITER_SLEEP', 'LOGMGR_QUEUE', 'ONDEMAND_TASK_QUEUE',
    'REQUEST_FOR_DEADLOCK_SEARCH', 'RESOURCE_QUEUE', 
    'SERVER_IDLE_CHECK', 'SLEEP_DBSTARTUP', 'SLEEP_DCOMSTARTUP',
    'SLEEP_MASTERDBREADY', 'SLEEP_MASTERMDREADY', 'SLEEP_MASTERUPGRADED',
    'SLEEP_MSDBSTARTUP', 'SLEEP_TEMPDBSTARTUP', 'SNI_HTTP_ACCEPT',
    'SP_SERVER_DIAGNOSTICS_SLEEP', 'SQLTRACE_BUFFER_FLUSH',
    'SQLTRACE_INCREMENTAL_FLUSH_SLEEP', 'WAITFOR',
    'WAIT_XTP_OFFLINE_CKPT_NEW_LOG', 'XE_DISPATCHER_WAIT',
    'XE_TIMER_EVENT', 'BROKER_EVENTHANDLER', 'CHECKPOINT_QUEUE',
    'DBMIRROR_EVENTS_QUEUE', 'SQLTRACE_WAIT_ENTRIES',
    'WAIT_XTP_CKPT_CLOSE', 'XE_DISPATCHER_JOIN'
)
ORDER BY wait_time_ms DESC;

-- Reset wait stats (để measure từ đầu)
DBCC SQLPERF('sys.dm_os_wait_stats', CLEAR);

Phân tích các Wait Types phổ biến

Wait TypeÝ nghĩaGiải pháp
PAGEIOLATCH_SH/EXĐọc/ghi trang từ diskThiếu RAM, I/O bottleneck, thiếu index
LCK_M_X, LCK_M_SChờ lock (blocking)Blocking chains, long transactions, thiếu index
CXPACKETParallel query waitsĐiều chỉnh MAXDOP, Cost Threshold for Parallelism
SOS_SCHEDULER_YIELDCPU pressureCPU overload, nhiều CPU-intensive queries
WRITELOGChờ log flushSlow disk (log file), high write workload
ASYNC_NETWORK_IOClient đọc dữ liệu chậmứng dụng xử lý kết quả chậm, large result sets
RESOURCE_SEMAPHOREChờ memory grantMemory pressure, sort/hash operations lớn
THREADPOOLKhông đủ workersCPU pressure, too many connections
PAGELATCH_*In-memory page contentionHotspot pages (GAM, PFS, identity columns)

5. sys.dm_os_waiting_tasks — Tasks đang chờ hiện tại

-- Xem blocking chains chi tiết
WITH BlockingChain AS (
    SELECT 
        wt.waiting_task_address,
        wt.session_id,
        wt.wait_type,
        wt.wait_duration_ms,
        wt.blocking_session_id,
        wt.resource_description,
        es.login_name,
        SUBSTRING(t.text, 1, 100) AS query_snippet
    FROM sys.dm_os_waiting_tasks wt
    JOIN sys.dm_exec_sessions es ON wt.session_id = es.session_id
    CROSS APPLY sys.dm_exec_sql_text(es.most_recent_sql_handle) t
    WHERE wt.blocking_session_id IS NOT NULL
)
SELECT 
    session_id AS blocked_session,
    blocking_session_id AS blocker,
    wait_type,
    wait_duration_ms / 1000.0 AS wait_seconds,
    login_name,
    query_snippet
FROM BlockingChain
ORDER BY wait_duration_ms DESC;

6. sys.dm_tran_locks — Locks hiện tại

-- Xem tất cả locks đang giữ
SELECT 
    l.resource_type,
    l.resource_database_id,
    l.resource_associated_entity_id,
    OBJECT_NAME(l.resource_associated_entity_id, l.resource_database_id) AS object_name,
    l.request_mode,   -- S, X, U, IS, IX, SIX, Sch-S, Sch-M
    l.request_status, -- GRANT, WAIT, CONVERT
    l.request_session_id,
    l.request_owner_type
FROM sys.dm_tran_locks l
WHERE l.resource_database_id = DB_ID()
    AND l.resource_type != 'DATABASE'
ORDER BY l.request_session_id, l.request_mode;

-- Xem deadlock information từ system_health XE session
SELECT 
    xdr.value('@timestamp', 'DATETIME2') AS deadlock_time,
    xdr.query('.') AS deadlock_graph
FROM (
    SELECT CAST(target_data AS XML) AS target_data
    FROM sys.dm_xe_session_targets t
    JOIN sys.dm_xe_sessions s ON t.event_session_address = s.address
    WHERE s.name = N'system_health'
      AND t.target_name = N'ring_buffer'
) AS data
CROSS APPLY target_data.nodes('//RingBufferTarget/event[@name="xml_deadlock_report"]') AS XEventData(xdr)
ORDER BY deadlock_time DESC;

7. sys.dm_io_virtual_file_stats — I/O per File

-- I/O statistics per database file
SELECT 
    DB_NAME(fs.database_id) AS database_name,
    mf.name AS file_name,
    mf.physical_name,
    mf.type_desc,
    fs.io_stall_read_ms,
    fs.num_of_reads,
    CASE fs.num_of_reads 
        WHEN 0 THEN 0 
        ELSE fs.io_stall_read_ms / fs.num_of_reads 
    END AS avg_read_ms,
    fs.io_stall_write_ms,
    fs.num_of_writes,
    CASE fs.num_of_writes 
        WHEN 0 THEN 0 
        ELSE fs.io_stall_write_ms / fs.num_of_writes 
    END AS avg_write_ms,
    fs.io_stall,
    fs.num_of_bytes_read / 1024 / 1024 AS mb_read,
    fs.num_of_bytes_written / 1024 / 1024 AS mb_written
FROM sys.dm_io_virtual_file_stats(NULL, NULL) fs
JOIN sys.master_files mf ON fs.database_id = mf.database_id 
    AND fs.file_id = mf.file_id
ORDER BY fs.io_stall DESC;

Rule of thumb: Avg read latency > 20ms → I/O bottleneck; > 50ms → nghiêm trọng.


8. sys.dm_db_index_usage_stats — Index Usage

-- Indexes ít được dùng (candidates để drop)
SELECT 
    OBJECT_NAME(ius.object_id) AS table_name,
    i.name AS index_name,
    i.type_desc,
    ius.user_seeks,
    ius.user_scans,
    ius.user_lookups,
    ius.user_updates,  -- Maintenance cost
    ius.last_user_seek,
    ius.last_user_scan
FROM sys.dm_db_index_usage_stats ius
JOIN sys.indexes i ON ius.object_id = i.object_id 
    AND ius.index_id = i.index_id
WHERE ius.database_id = DB_ID()
    AND i.type > 0  -- Không phải heap
    AND ius.user_seeks + ius.user_scans + ius.user_lookups = 0  -- Không được dùng
    AND ius.user_updates > 0  -- Nhưng vẫn tốn maintenance cost
ORDER BY ius.user_updates DESC;

-- Index được dùng nhiều nhất
SELECT TOP 20
    OBJECT_NAME(ius.object_id) AS table_name,
    i.name AS index_name,
    ius.user_seeks + ius.user_scans + ius.user_lookups AS total_reads,
    ius.user_updates AS total_writes,
    CAST(100.0 * ius.user_seeks / 
        NULLIF(ius.user_seeks + ius.user_scans + ius.user_lookups, 0) 
        AS DECIMAL(5,2)) AS seek_pct
FROM sys.dm_db_index_usage_stats ius
JOIN sys.indexes i ON ius.object_id = i.object_id 
    AND ius.index_id = i.index_id
WHERE ius.database_id = DB_ID()
ORDER BY total_reads DESC;

9. sys.dm_db_missing_index_details — Missing Index Suggestions

-- Missing indexes được SQL Server đề xuất
SELECT TOP 20
    DB_NAME(mid.database_id) AS database_name,
    OBJECT_NAME(mid.object_id, mid.database_id) AS table_name,
    mig.avg_user_impact,  -- % improvement estimate
    mig.user_seeks,
    mig.user_scans,
    mig.avg_total_user_cost,  -- Average query cost without index
    mid.equality_columns,
    mid.inequality_columns,
    mid.included_columns,
    -- Câu lệnh tạo index gợi ý
    'CREATE NONCLUSTERED INDEX IX_' + 
        OBJECT_NAME(mid.object_id, mid.database_id) + '_' +
        REPLACE(REPLACE(ISNULL(mid.equality_columns, '') + 
            ISNULL(mid.inequality_columns, ''), '[', ''), ']', '') +
    ' ON ' + mid.statement + 
    ' (' + ISNULL(mid.equality_columns, '') + 
        CASE WHEN mid.inequality_columns IS NOT NULL THEN 
            CASE WHEN mid.equality_columns IS NOT NULL THEN ', ' ELSE '' END + 
            mid.inequality_columns 
        ELSE '' END + ')' + 
    CASE WHEN mid.included_columns IS NOT NULL THEN 
        ' INCLUDE (' + mid.included_columns + ')' 
    ELSE '' END AS suggested_create_index
FROM sys.dm_db_missing_index_details mid
JOIN sys.dm_db_missing_index_groups mig ON mid.index_handle = mig.index_handle
JOIN sys.dm_db_missing_index_group_stats migs ON mig.index_group_handle = migs.group_handle
WHERE mid.database_id = DB_ID()
ORDER BY mig.avg_user_impact * mig.user_seeks DESC;

Cảnh báo: Đây chỉ là gợi ý — KHÔNG tạo tất cả missing indexes đề xuất. Validate với workload thực tế.


Query Store (SQL Server 2016+)

Query Store tự động collect query plans, runtime stats — không cần traces.

Bật Query Store

-- Enable Query Store
ALTER DATABASE ProductionDB
SET QUERY_STORE = ON (
    OPERATION_MODE = READ_WRITE,
    CLEANUP_POLICY = (STALE_QUERY_THRESHOLD_DAYS = 30),
    DATA_FLUSH_INTERVAL_SECONDS = 900,
    INTERVAL_LENGTH_MINUTES = 60,
    MAX_STORAGE_SIZE_MB = 1000,
    QUERY_CAPTURE_MODE = AUTO,  -- AUTO = chỉ capture queries đáng kể
    SIZE_BASED_CLEANUP_MODE = AUTO
);

-- Xem trạng thái
SELECT * FROM sys.database_query_store_options;

Phát hiện Plan Regression

-- Tìm queries có plan regression (plan mới chậm hơn plan cũ)
WITH PlanStats AS (
    SELECT 
        q.query_id,
        qt.query_sql_text,
        p.plan_id,
        rs.avg_duration,
        rs.avg_cpu_time,
        rs.avg_logical_io_reads,
        rs.count_executions,
        rs.first_execution_time,
        ROW_NUMBER() OVER (PARTITION BY q.query_id 
                           ORDER BY rs.avg_duration) AS plan_rank
    FROM sys.query_store_query q
    JOIN sys.query_store_query_text qt ON q.query_text_id = qt.query_text_id
    JOIN sys.query_store_plan p ON q.query_id = p.query_id
    JOIN sys.query_store_runtime_stats rs ON p.plan_id = rs.plan_id
    JOIN sys.query_store_runtime_stats_interval rsi 
        ON rs.runtime_stats_interval_id = rsi.runtime_stats_interval_id
    WHERE rsi.start_time >= DATEADD(HOUR, -24, GETUTCDATE())
)
SELECT 
    ps_fast.query_id,
    SUBSTRING(ps_fast.query_sql_text, 1, 100) AS query_snippet,
    ps_fast.plan_id AS fast_plan_id,
    ps_fast.avg_duration AS fast_plan_avg_duration_us,
    ps_slow.plan_id AS current_plan_id,
    ps_slow.avg_duration AS current_plan_avg_duration_us,
    CAST(ps_slow.avg_duration / NULLIF(ps_fast.avg_duration, 0) AS DECIMAL(10,2)) AS regression_ratio
FROM PlanStats ps_fast
JOIN PlanStats ps_slow ON ps_fast.query_id = ps_slow.query_id
WHERE ps_fast.plan_rank = 1
    AND ps_slow.plan_rank > 1
    AND ps_slow.avg_duration > ps_fast.avg_duration * 2  -- 2x slower = regression
ORDER BY regression_ratio DESC;

-- Force một plan cụ thể
EXEC sys.sp_query_store_force_plan @query_id = 42, @plan_id = 7;

-- Unforce plan
EXEC sys.sp_query_store_unforce_plan @query_id = 42, @plan_id = 7;

-- Top consuming queries trong Query Store
SELECT TOP 10
    q.query_id,
    SUBSTRING(qt.query_sql_text, 1, 100) AS query_snippet,
    SUM(rs.avg_duration * rs.count_executions) AS total_duration_us,
    SUM(rs.count_executions) AS total_executions,
    SUM(rs.avg_cpu_time * rs.count_executions) AS total_cpu_us
FROM sys.query_store_query q
JOIN sys.query_store_query_text qt ON q.query_text_id = qt.query_text_id
JOIN sys.query_store_plan p ON q.query_id = p.query_id
JOIN sys.query_store_runtime_stats rs ON p.plan_id = rs.plan_id
GROUP BY q.query_id, qt.query_sql_text
ORDER BY total_duration_us DESC;

Extended Events

Extended Events (XE) là hệ thống tracing nhẹ, thay thế SQL Server Profiler.

-- Tạo XE session để capture slow queries
CREATE EVENT SESSION [CaptureSlowQueries] ON SERVER
ADD EVENT sqlserver.sql_statement_completed(
    WHERE (
        [duration] > 1000000  -- > 1 giây (microseconds)
        AND [cpu_time] > 100000  -- > 100ms CPU
    )
    ACTION(
        sqlserver.sql_text,
        sqlserver.query_hash,
        sqlserver.query_plan_hash,
        sqlserver.client_app_name,
        sqlserver.username,
        sqlserver.database_name
    )
),
ADD EVENT sqlserver.rpc_completed(
    WHERE ([duration] > 1000000)
    ACTION(sqlserver.sql_text, sqlserver.database_name)
)
ADD TARGET package0.event_file(
    SET filename = N'D:\XEvents\SlowQueries.xel',
    max_file_size = 500,   -- 500 MB per file
    max_rollover_files = 5  -- Keep last 5 files
)
WITH (
    MAX_MEMORY = 50 MB,
    EVENT_RETENTION_MODE = ALLOW_SINGLE_EVENT_LOSS,
    MAX_DISPATCH_LATENCY = 5 SECONDS,
    MAX_EVENT_SIZE = 0 KB,
    MEMORY_PARTITION_MODE = NONE,
    TRACK_CAUSALITY = OFF,
    STARTUP_STATE = OFF
);

-- Start session
ALTER EVENT SESSION [CaptureSlowQueries] ON SERVER STATE = START;

-- Tạo session để capture deadlocks (thay thế cho Profiler trace)
CREATE EVENT SESSION [CaptureDeadlocks] ON SERVER
ADD EVENT sqlserver.xml_deadlock_report
ADD TARGET package0.ring_buffer(SET max_memory = 51200)
WITH (STARTUP_STATE = ON);  -- Auto-start với SQL Server

ALTER EVENT SESSION [CaptureDeadlocks] ON SERVER STATE = START;

-- Đọc deadlock data từ ring buffer
SELECT 
    xdr.value('@timestamp', 'DATETIME2') AS deadlock_time,
    xdr.query('.') AS deadlock_xml
FROM (
    SELECT CAST(target_data AS XML) AS target_data
    FROM sys.dm_xe_session_targets t
    JOIN sys.dm_xe_sessions s ON t.event_session_address = s.address
    WHERE s.name = N'CaptureDeadlocks'
      AND t.target_name = N'ring_buffer'
) AS data
CROSS APPLY target_data.nodes('//RingBufferTarget/event[@name="xml_deadlock_report"]') XEventData(xdr)
ORDER BY deadlock_time DESC;

-- Đọc XE file
SELECT 
    event_data.value('(event/@name)[1]', 'NVARCHAR(50)') AS event_name,
    event_data.value('(event/@timestamp)[1]', 'DATETIME2') AS event_time,
    event_data.value('(event/data[@name="duration"]/value)[1]', 'BIGINT') / 1000 AS duration_ms,
    event_data.value('(event/action[@name="sql_text"]/value)[1]', 'NVARCHAR(MAX)') AS sql_text
FROM (
    SELECT CAST(event_data AS XML) AS event_data
    FROM sys.fn_xe_file_target_read_file(
        'D:\XEvents\SlowQueries*.xel', NULL, NULL, NULL)
) AS XEData
ORDER BY event_time DESC;

-- Stop và drop session
ALTER EVENT SESSION [CaptureSlowQueries] ON SERVER STATE = STOP;
DROP EVENT SESSION [CaptureSlowQueries] ON SERVER;

SQL Server Error Log

-- Đọc error log hiện tại
EXEC xp_readerrorlog 0, 1;  -- 0=current log, 1=error log (2=agent log)

-- Filter by keyword và time range
EXEC xp_readerrorlog 
    0,           -- Log file number (0=current)
    1,           -- Log type (1=SQL, 2=Agent)
    N'Error',    -- Search string 1
    NULL,        -- Search string 2
    '2026-04-01 00:00:00',  -- Start time
    '2026-04-01 23:59:59',  -- End time
    N'DESC';     -- Sort order

-- Cycle error log (tạo log mới, archive cũ)
EXEC sp_cycle_errorlog;

-- Xem số lượng error logs được giữ
EXEC xp_instance_regread 
    N'HKEY_LOCAL_MACHINE',
    N'Software\Microsoft\MSSQLServer\MSSQLServer',
    N'NumErrorLogs';
-- Config qua SSMS: Management > SQL Server Logs > Configure

Windows Performance Monitor Counters

Key SQL Server PerfMon counters cần monitor:

CounterNormalCảnh báoNghiêm trọng
Buffer Cache Hit Ratio> 99%95-99%< 95%
Page Life Expectancy> 300s (tốt hơn > 1000s)200-300s< 200s
Batch Requests/secBaseline20% trên baseline50% trên baseline
SQL Compilations/sec< 10% của Batch Req10-20%> 20% (thiếu reuse/parameterization)
SQL Re-Compilations/sec< 10% của Compilations
Lock Waits/sec0 ideally> 0 thường xuyên = blocking
Deadlocks/sec0AnyNhiều = design issue
Checkpoint Pages/secVariesCao liên tục = dirty page pressure
Lazy Writes/sec0> 0 = memory pressure
-- Lấy PLE qua DMV (không cần PerfMon external)
SELECT 
    object_name,
    counter_name,
    instance_name,
    cntr_value
FROM sys.dm_os_performance_counters
WHERE counter_name IN (
    'Page life expectancy',
    'Batch Requests/sec',
    'SQL Compilations/sec',
    'SQL Re-Compilations/sec',
    'Lock Waits/sec',
    'Full Scans/sec',
    'Index Searches/sec',
    'Deadlocks/sec',
    'Buffer cache hit ratio'
)
ORDER BY object_name, counter_name;

Activity Monitor

Activity Monitor trong SSMS (Right-click server → Activity Monitor) cung cấp:

  • Overview: CPU, waits, I/O, batch requests/sec (real-time chart)
  • Processes: active sessions, blocking chains
  • Resource Waits: wait types heatmap
  • Data File I/O: read/write per file
  • Recent Expensive Queries: top queries hiện tại

Lưu ý: Activity Monitor có overhead nhất định, không nên để mở liên tục trên production.


Thiết lập Performance Baseline

-- Script thu thập baseline (chạy theo lịch để trend analysis)
CREATE TABLE dbo.PerformanceBaseline (
    CaptureTime     DATETIME2 DEFAULT SYSDATETIME(),
    CounterName     NVARCHAR(128),
    InstanceName    NVARCHAR(128),
    CounterValue    BIGINT
);

-- Insert snapshot
INSERT INTO dbo.PerformanceBaseline (CounterName, InstanceName, CounterValue)
SELECT counter_name, instance_name, cntr_value
FROM sys.dm_os_performance_counters
WHERE counter_name IN (
    'Page life expectancy', 
    'Batch Requests/sec',
    'SQL Compilations/sec'
);

-- Wait stats snapshot
CREATE TABLE dbo.WaitStatsBaseline (
    CaptureTime     DATETIME2 DEFAULT SYSDATETIME(),
    WaitType        NVARCHAR(60),
    WaitTimeMs      BIGINT,
    WaitingTasks    BIGINT
);

INSERT INTO dbo.WaitStatsBaseline (WaitType, WaitTimeMs, WaitingTasks)
SELECT wait_type, wait_time_ms, waiting_tasks_count
FROM sys.dm_os_wait_stats
WHERE wait_type NOT IN ('SLEEP_TASK', 'LAZYWRITER_SLEEP', 'WAITFOR');

-- So sánh 2 snapshots để tính delta
SELECT 
    curr.WaitType,
    curr.WaitTimeMs - prev.WaitTimeMs AS delta_wait_ms,
    curr.WaitingTasks - prev.WaitingTasks AS delta_tasks
FROM dbo.WaitStatsBaseline curr
JOIN dbo.WaitStatsBaseline prev ON curr.WaitType = prev.WaitType
WHERE curr.CaptureTime = '2026-04-01 12:00:00'
    AND prev.CaptureTime = '2026-04-01 11:00:00'
ORDER BY delta_wait_ms DESC;

Q&A - Phỏng vấn Monitoring & Diagnostics

Junior Level

Q1: DMV là gì? Tại sao quan trọng?

Dynamic Management Views (DMVs) là các views hệ thống trong SQL Server cung cấp thông tin runtime về server health, query performance, waits, locks. Quan trọng vì:

  • Real-time diagnostic mà không cần external tools
  • Phát hiện blocking, long-running queries, missing indexes
  • Không cần đặc quyền cao (một số DMV chỉ cần VIEW SERVER STATE)
  • Dữ liệu accumulate từ khi SQL Server restart (trừ một số reset khi plan cache clear)

Q2: Làm thế nào để tìm query đang blocking trong SQL Server?

SELECT r.session_id, r.blocking_session_id, t.text
FROM sys.dm_exec_requests r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
WHERE r.blocking_session_id > 0;

Sau đó dùng KILL session_id để giải phóng nếu thực sự cần.

Q3: Sự khác biệt giữa Extended Events và SQL Server Profiler?

Extended EventsSQL Server Profiler
Performance overheadRất thấpCao (không dùng trên production)
GranularityRất chi tiếtHạn chế hơn
Storage targetsRing buffer, file, ETWFile, table
Real-time
StatusHiện tại (recommended)Legacy (deprecated)

Mid Level

Q4: Giải thích wait statistics. Làm thế nào dùng để chẩn đoán bottleneck?

Khi SQL Server cần tài nguyên nhưng không available (CPU, I/O, lock, memory), task sẽ wait và ghi vào wait stats. Analyze top wait types để biết bottleneck:

  • PAGEIOLATCH_*: data pages đang đọc từ disk → thêm RAM hoặc index
  • LCK_M_*: blocking → tìm blocker, tối ưu transactions
  • CXPACKET: parallel queries đang sync → điều chỉnh MAXDOP
  • RESOURCE_SEMAPHORE: sort/hash cần memory grant → tối ưu queries, thêm RAM

Q5: Query Store khác gì với sys.dm_exec_query_stats?

Query Storesys.dm_exec_query_stats
PersistencePersisted trong databaseLost khi plan evicted from cache
HistoryConfigurable (days/weeks)Chỉ plans còn trong cache
Plan comparisonCó thể compare nhiều plans per queryMột plan per entry
Force planKhông
OverheadNhỏNone (passive)

Q6: Page Life Expectancy (PLE) là gì? Giá trị bao nhiêu là tốt?

PLE đo số giây trung bình một data page ở trong buffer pool trước khi bị evict. PLE thấp = pages thường xuyên phải đọc lại từ disk → I/O pressure.

  • Traditional threshold: > 300 giây
  • Modern guidance: baseline thường > 1000 giây; cảnh báo khi drop > 50% so với baseline
  • Với nhiều NUMA nodes: PLE per NUMA node quan trọng hơn global PLE

Senior Level

Q7: Xây dựng monitoring strategy cho SQL Server production. Những gì cần monitor và alert?

Real-time alerts (immediate response):

  • Deadlocks/sec > 0 → XE capture + notify DBA
  • Blocking duration > 5 phút → auto-kill hoặc notify
  • CPU > 90% trong 5+ phút
  • Error log: severity 17+ errors

Trending metrics (collect baseline, alert on deviation):

  • PLE trend (daily average, alert if < 300s)
  • Wait stats delta (hourly snapshot comparison)
  • I/O latency per file (> 20ms read = investigate)
  • Disk free space (< 20% = warning, < 10% = critical)
  • Log space usage (per database)
  • TempDB space usage

Capacity planning (weekly/monthly):

  • Database growth rate
  • Index fragmentation trends
  • Missing index opportunities
  • Top growing tables

Tools: SQL Server Central Monitoring Repository (custom), Ola Hallengren + alerts, third-party (SolarWinds DPA, SentryOne/SQL Sentry, Redgate SQL Monitor)

Q8: Làm thế nào để diagnose một query đột ngột chậm hơn mà không reproduce được?

  1. Query Store (nếu đang bật): tìm query_id, so sánh plans, elapsed time trend theo thời gian. Nếu plan regression → force old plan
  2. sys.dm_exec_query_stats: xem plan_generation_num (số lần recompile), execution count, avg duration
  3. Extended Events: capture query_post_execution_showplan để xem actual plan
  4. Wait stats per query: dùng SET STATISTICS IO, TIME ON để reproduce
  5. Parameter sniffing: thử OPTION (RECOMPILE) hoặc OPTION (OPTIMIZE FOR UNKNOWN)
  6. Statistics: kiểm tra last_updated của statistics trên các tables liên quan — outdated stats → bad estimates → bad plan
  7. Schema changes: ai đó drop/create index, thay đổi table structure

Q9: Explain tại sao CXPACKET wait cao không nhất thiết là vấn đề. Phân biệt CXPACKET “healthy” và “unhealthy”?

CXPACKET là wait type khi các threads trong parallel query plan đang sync (exchange operators). SQL Server phải balance work giữa các threads.

“Healthy” CXPACKET:

  • Parallel query đang chạy nhanh, threads sync là bình thường
  • CXPACKET is high nhưng query duration ngắn

“Unhealthy” CXPACKET:

  • Thread skew: một thread xử lý 90% work, các thread khác chờ → CXPACKET cao + query chậm
  • Thường kèm SOS_SCHEDULER_YIELD (CPU pressure)
  • Nguyên nhân: data skew, bad statistics, non-sargable predicates

Giải pháp:

  • Điều chỉnh Cost Threshold for Parallelism (mặc định 5, thường tăng lên 50-75)
  • MAXDOP per query: OPTION (MAXDOP 4) để limit parallelism
  • Fix statistics và data skew
  • SQL Server 2019+: Intelligent Query Processing tự adjust parallelism

Từ SQL Server 2016 SP2+: CXCONSUMER (idle threads waiting) thay thế cho phần idle của CXPACKET, giúp phân tích chính xác hơn.

Advanced Features - Tính năng Nâng cao

Giới thiệu

Section này bao gồm các tính năng nâng cao của SQL Server thường gặp trong phỏng vấn Senior/Mid level. Mỗi tính năng có file riêng với nội dung chi tiết.


Danh sách Sub-topics

Tính năngMô tảFile
PartitioningChia dữ liệu bảng theo partition key, partition elimination, switchingpartitioning.md
JSON & XMLLưu trữ, truy vấn, export dữ liệu JSON/XMLjson-xml.md
High AvailabilityAlways On AG, FCI, Log Shipping, Mirroring, Replicationhigh-availability.md
Backup & RecoveryRecovery models, backup types, restore, RPO/RTObackup-recovery.md
MonitoringDMVs, Query Store, Extended Events, Wait Statsmonitoring-diagnostics.md
Temporal TablesSystem-versioned tables, history trackingXem bên dưới
Full-Text SearchFull-text indexes, predicates CONTAINS/FREETEXTXem bên dưới
In-Memory OLTPMemory-optimized tables, natively compiled procsXem bên dưới
Change Data CaptureCDC, Change TrackingXem bên dưới
Query StorePlan forcing, regression detectionmonitoring-diagnostics.md

Temporal Tables (System-Versioned)

Tự động lưu lịch sử thay đổi dữ liệu — built-in audit trail.

-- Tạo temporal table
CREATE TABLE dbo.Employees (
    EmployeeID      INT PRIMARY KEY,
    Name            NVARCHAR(100) NOT NULL,
    Department      NVARCHAR(50),
    Salary          DECIMAL(12, 2),
    -- Hai cột system-period BẮTBUỘC
    ValidFrom       DATETIME2 GENERATED ALWAYS AS ROW START NOT NULL,
    ValidTo         DATETIME2 GENERATED ALWAYS AS ROW END NOT NULL,
    PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo)
)
WITH (SYSTEM_VERSIONING = ON (
    HISTORY_TABLE = dbo.EmployeesHistory  -- SQL Server tự tạo nếu không tồn tại
));

-- DML operations tự động lưu history
UPDATE dbo.Employees SET Salary = 80000 WHERE EmployeeID = 1;
-- Bản ghi cũ tự động chuyển sang dbo.EmployeesHistory

DELETE FROM dbo.Employees WHERE EmployeeID = 2;
-- Deleted row lưu trong history

-- Query thời điểm hiện tại (thông thường)
SELECT * FROM dbo.Employees;

-- Query tại một thời điểm trong quá khứ
SELECT * FROM dbo.Employees
FOR SYSTEM_TIME AS OF '2025-01-01T00:00:00';

-- Query trong một khoảng thời gian (tất cả versions)
SELECT * FROM dbo.Employees
FOR SYSTEM_TIME BETWEEN '2025-01-01' AND '2026-01-01';

-- Query tất cả rows bao gồm đã xóa trong khoảng
SELECT * FROM dbo.Employees
FOR SYSTEM_TIME FROM '2025-01-01' TO '2026-01-01';

-- Xem lịch sử thay đổi của một nhân viên cụ thể
SELECT 
    EmployeeID, Name, Department, Salary,
    ValidFrom, ValidTo,
    CASE WHEN ValidTo = '9999-12-31 23:59:59.9999999' 
         THEN 'Current' ELSE 'Historical' END AS RecordType
FROM dbo.Employees
FOR SYSTEM_TIME ALL
WHERE EmployeeID = 1
ORDER BY ValidFrom;

-- Disable và enable system versioning
ALTER TABLE dbo.Employees SET (SYSTEM_VERSIONING = OFF);
ALTER TABLE dbo.Employees SET (SYSTEM_VERSIONING = ON (
    HISTORY_TABLE = dbo.EmployeesHistory,
    DATA_CONSISTENCY_CHECK = ON
));

Temporal Table Use Cases

  • Audit trail: ai sửa gì, khi nào
  • Point-in-time reporting: báo cáo số liệu cuối tháng, cuối năm
  • Slowly Changing Dimensions (SCD) trong data warehouse
  • Regulatory compliance: GDPR, SOX — lưu lịch sử thay đổi

Tìm kiếm văn bản nâng cao hơn LIKE — linguistic analysis, word inflections, thesaurus.

-- Bước 1: Enable Full-Text trên database
-- CREATE FULLTEXT CATALOG FullTextCatalog AS DEFAULT;

-- Bước 2: Tạo Full-Text Index
CREATE FULLTEXT CATALOG FTCatalog AS DEFAULT;

CREATE FULLTEXT INDEX ON dbo.Articles (
    Title LANGUAGE 1033,    -- English
    Body LANGUAGE 1033
)
KEY INDEX PK_Articles
ON FTCatalog
WITH CHANGE_TRACKING AUTO;  -- Auto sync với DML

-- CONTAINS: tìm từ/cụm từ chính xác
SELECT ArticleID, Title
FROM dbo.Articles
WHERE CONTAINS(Body, '"SQL Server"');

-- Proximity: hai từ gần nhau
SELECT * FROM dbo.Articles
WHERE CONTAINS(Body, '"performance" NEAR "optimization"');

-- Inflectional forms: "run" tìm cả "ran", "running", "runs"
SELECT * FROM dbo.Articles
WHERE CONTAINS(Body, 'FORMSOF(INFLECTIONAL, run)');

-- Thesaurus: "car" tìm cả "automobile", "vehicle"
SELECT * FROM dbo.Articles
WHERE CONTAINS(Body, 'FORMSOF(THESAURUS, car)');

-- Weighted search: boost title hơn body
SELECT ArticleID, Title, 
    KEY_TBL.RANK
FROM dbo.Articles
INNER JOIN CONTAINSTABLE(dbo.Articles, (Title, Body), 
    'SQL Server performance', 10) AS KEY_TBL
ON dbo.Articles.ArticleID = KEY_TBL.[KEY]
ORDER BY KEY_TBL.RANK DESC;

-- FREETEXT: ngôn ngữ tự nhiên (less precise, more recall)
SELECT ArticleID, Title
FROM dbo.Articles
WHERE FREETEXT(Body, 'how to improve database performance');

-- FREETEXTTABLE: với ranking
SELECT a.Title, kt.RANK
FROM dbo.Articles a
JOIN FREETEXTTABLE(dbo.Articles, Body, 
    'database tuning optimization') AS kt
ON a.ArticleID = kt.[KEY]
ORDER BY kt.RANK DESC;

Full-Text vs LIKE vs Elasticsearch

LIKEFull-Text SearchElasticsearch
Wildcards%word%
Stemming (inflections)Không
Relevance rankingKhông
PerformanceChậm (full scan)Nhanh (inverted index)Rất nhanh
Language analysisKhông
ScalabilityHạn chếTrung bìnhTốt
ComplexityĐơn giảnTrung bìnhCao

In-Memory OLTP (Hekaton)

Memory-optimized tables và natively compiled stored procedures — tốc độ cao nhất.

-- Bước 1: Thêm memory-optimized filegroup
ALTER DATABASE SalesDB 
ADD FILEGROUP SalesDB_InMem CONTAINS MEMORY_OPTIMIZED_DATA;

ALTER DATABASE SalesDB ADD FILE (
    NAME = 'SalesDB_InMem',
    FILENAME = 'D:\SQL\SalesDB_InMem'
) TO FILEGROUP SalesDB_InMem;

-- Bước 2: Tạo memory-optimized table
CREATE TABLE dbo.ShoppingCart (
    CartID      INT NOT NULL,
    UserID      INT NOT NULL,
    ProductID   INT NOT NULL,
    Quantity    INT NOT NULL,
    AddedDate   DATETIME2 NOT NULL DEFAULT SYSDATETIME(),
    CONSTRAINT PK_ShoppingCart PRIMARY KEY NONCLUSTERED 
        HASH (CartID) WITH (BUCKET_COUNT = 1000000)
    -- HASH index: O(1) lookups; Bucket count = expected rows * 1-2
)
WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA);
-- DURABILITY: SCHEMA_AND_DATA (persistent) hoặc SCHEMA_ONLY (temp, fast)

-- Tạo nonclustered index trên memory-optimized table
CREATE INDEX IX_Cart_UserID ON dbo.ShoppingCart (UserID)
WITH (BUCKET_COUNT = 500000);

-- Natively compiled stored procedure (compiled to machine code)
CREATE OR ALTER PROCEDURE dbo.usp_AddToCart
    @CartID INT, @UserID INT, @ProductID INT, @Quantity INT
WITH NATIVE_COMPILATION, SCHEMABINDING
AS
BEGIN ATOMIC WITH (
    TRANSACTION ISOLATION LEVEL = SNAPSHOT,
    LANGUAGE = N'us_english'
)
    INSERT INTO dbo.ShoppingCart (CartID, UserID, ProductID, Quantity)
    VALUES (@CartID, @UserID, @ProductID, @Quantity);
END;

-- Regular DML trên memory-optimized table (không cần native compilation)
INSERT INTO dbo.ShoppingCart VALUES (1, 1001, 5, 2, DEFAULT);
SELECT * FROM dbo.ShoppingCart WHERE UserID = 1001;

-- Monitor memory usage
SELECT 
    object_name(object_id) AS table_name,
    memory_allocated_for_table_kb,
    memory_used_by_table_kb,
    memory_allocated_for_indexes_kb,
    memory_used_by_indexes_kb
FROM sys.dm_db_xtp_table_memory_stats
WHERE database_id = DB_ID();

In-Memory OLTP - Khi nào dùng?

Phù hợp:

  • High-concurrency OLTP (nhiều insert/update/delete cùng lúc)
  • Session state, shopping carts, queue tables
  • Short-lived transient data
  • Eliminate latch contention (TempDB hotspots)

Giới hạn:

  • Không hỗ trợ ALTER TABLE (phải drop/recreate)
  • Không hỗ trợ một số T-SQL features (joins trong natively compiled, subqueries)
  • FK constraints phức tạp
  • Dữ liệu phải fit trong RAM (với DURABILITY = SCHEMA_AND_DATA vẫn persistent)

Change Data Capture (CDC)

Capture INSERT, UPDATE, DELETE thay đổi vào change tables — phổ biến trong ETL.

-- Enable CDC ở database level
USE SalesDB;
EXEC sys.sp_cdc_enable_db;

-- Enable CDC cho một table
EXEC sys.sp_cdc_enable_table
    @source_schema = N'dbo',
    @source_name = N'Orders',
    @role_name = N'cdc_admin',  -- NULL = không restrict
    @supports_net_changes = 1,  -- Tổng hợp net change per row
    @captured_column_list = NULL;  -- NULL = capture tất cả columns

-- CDC tạo các change tables tự động:
-- cdc.dbo_Orders_CT: bảng chứa thay đổi
-- Columns: __$start_lsn, __$end_lsn, __$seqval, __$operation, __$update_mask, + all source columns
-- __$operation: 1=DELETE, 2=INSERT, 3=Before UPDATE, 4=After UPDATE

-- Query changes trong một LSN range
DECLARE @from_lsn BINARY(10) = sys.fn_cdc_get_min_lsn('dbo_Orders');
DECLARE @to_lsn   BINARY(10) = sys.fn_cdc_get_max_lsn();

SELECT 
    __$operation,
    CASE __$operation 
        WHEN 1 THEN 'DELETE' 
        WHEN 2 THEN 'INSERT' 
        WHEN 3 THEN 'Before UPDATE'
        WHEN 4 THEN 'After UPDATE'
    END AS operation_name,
    OrderID, CustomerID, TotalAmount, OrderDate
FROM cdc.fn_cdc_get_all_changes_dbo_Orders(
    @from_lsn, @to_lsn, N'all');

-- Net changes (chỉ kết quả cuối cùng per row)
SELECT * 
FROM cdc.fn_cdc_get_net_changes_dbo_Orders(
    @from_lsn, @to_lsn, N'all with mask');

-- Convert LSN sang thời gian
SELECT sys.fn_cdc_map_lsn_to_time(@from_lsn) AS from_time,
       sys.fn_cdc_map_lsn_to_time(@to_lsn) AS to_time;

-- Convert thời gian sang LSN
DECLARE @since_lsn BINARY(10) = 
    sys.fn_cdc_map_time_to_lsn('smallest greater than or equal', 
        '2026-04-01 00:00:00');

-- Disable CDC
EXEC sys.sp_cdc_disable_table
    @source_schema = N'dbo',
    @source_name = N'Orders',
    @capture_instance = N'dbo_Orders';

Change Tracking (nhẹ hơn CDC)

-- Enable Change Tracking ở database level
ALTER DATABASE SalesDB
SET CHANGE_TRACKING = ON (
    CHANGE_RETENTION = 7 DAYS,  -- Giữ history 7 ngày
    AUTO_CLEANUP = ON
);

-- Enable cho table
ALTER TABLE dbo.Orders
ENABLE CHANGE_TRACKING WITH (TRACK_COLUMNS_UPDATED = ON);

-- Query changes (chỉ biết row nào đổi, không biết value cũ)
DECLARE @sync_version BIGINT = CHANGE_TRACKING_MIN_VALID_VERSION(OBJECT_ID('dbo.Orders'));
DECLARE @last_sync_version BIGINT = 0;  -- Lưu version từ lần sync trước

SELECT 
    o.OrderID,
    o.CustomerID,
    o.TotalAmount,
    CT.SYS_CHANGE_OPERATION,  -- I=Insert, U=Update, D=Delete
    CT.SYS_CHANGE_VERSION,
    CT.SYS_CHANGE_COLUMNS  -- Bitmap: columns bị thay đổi
FROM CHANGETABLE(CHANGES dbo.Orders, @last_sync_version) AS CT
LEFT JOIN dbo.Orders o ON o.OrderID = CT.OrderID;

-- Lấy current version để save cho lần sync kế tiếp
SELECT CHANGE_TRACKING_CURRENT_VERSION() AS current_version;

CDC vs Change Tracking

CDCChange Tracking
Data capturedBefore/after values + DML typeChỉ primary key + operation type
StorageChange tables (nhiều storage)Internal tables (ít storage)
HistoryLâu dài (configurable retention)Short-term (7 days thường)
Use caseETL, auditing, event sourcingSync, incremental loads
OverheadCao hơnThấp hơn
GranularityRow + column levelRow level

Q&A - Advanced Features Tổng hợp

Junior Level

Q1: Temporal Tables trong SQL Server là gì? Use case?

Temporal tables (system-versioned) tự động lưu lịch sử thay đổi dữ liệu. SQL Server tự động ghi row cũ vào history table khi có UPDATE/DELETE.

Use cases: audit trail, point-in-time reporting, regulatory compliance (ai sửa gì lúc mấy giờ), slowly changing dimensions trong data warehouse.

Q2: CONTAINS và FREETEXT khác nhau thế nào?

  • CONTAINS: tìm kiếm chính xác theo terms, phrases, proximity. Cho phép boolean operators (AND, OR, AND NOT). Precision cao hơn
  • FREETEXT: tìm kiếm ngôn ngữ tự nhiên — SQL Server analyze câu và tìm các từ có ý nghĩa tương tự. Recall cao hơn, ít control hơn

Q3: CDC và Change Tracking khác nhau thế nào?

CDC capture đầy đủ before/after values và DML type — dùng cho ETL, auditing cần biết giá trị thay đổi. Change Tracking chỉ track primary key và operation type (I/U/D) — nhẹ hơn, dùng cho sync scenarios khi chỉ cần biết row nào thay đổi.

Q4: In-Memory OLTP (memory-optimized tables) có ưu điểm gì?

  • Loại bỏ latch contention (không có pages/locks theo nghĩa truyền thống)
  • Natively compiled stored procedures chạy nhanh hơn interpreted T-SQL
  • Optimistic concurrency (SNAPSHOT isolation mặc định)
  • Throughput cao hơn nhiều cho high-concurrent OLTP workloads

Mid Level

Q5: Cách query dữ liệu trong Temporal Table tại một thời điểm trong quá khứ?

Dùng FOR SYSTEM_TIME AS OF 'datetime':

SELECT * FROM dbo.Employees
FOR SYSTEM_TIME AS OF '2025-06-30T23:59:59';

SQL Server tự động query cả bảng hiện tại và history table để tìm rows có ValidFrom <= datetime < ValidTo.

Q6: Memory-optimized table dùng HASH index vs RANGE index — khi nào dùng cái nào?

  • HASH index: O(1) point lookup cho equality predicates (WHERE ID = ?). Cần chỉ định BUCKET_COUNT (nên >= expected rows). Không hỗ trợ range scans hay ORDER BY
  • NONCLUSTERED index (trong memory-optimized): hỗ trợ range scans và ordering, nhưng chậm hơn HASH cho point lookups

Quy tắc: nếu query chủ yếu là equality lookup → HASH; nếu cần range scan hoặc ORDER BY → NONCLUSTERED.

Q7: CDC sử dụng LSN — LSN là gì và tại sao quan trọng trong CDC?

LSN (Log Sequence Number) là identifier duy nhất và tăng dần cho mỗi log record trong transaction log. CDC dùng LSN để:

  • Track exactly which changes đã được xử lý (idempotent incremental loads)
  • Convert sang thời gian: sys.fn_cdc_map_lsn_to_time()
  • Query changes trong range: fn_cdc_get_all_changes_*(from_lsn, to_lsn)

Trong ETL, sau mỗi batch bạn lưu to_lsn làm điểm bắt đầu cho batch kế tiếp → không bỏ sót và không duplicate changes.

Q8: Tại sao không nên dùng Full-Text Search cho tất cả text queries?

Full-Text có overhead:

  • Index maintenance (sync change tables)
  • Disk space cho FT indexes
  • Linguistic analysis complexity

Dùng FT khi: cần tìm kiếm ngôn ngữ tự nhiên, stemming, proximity, relevance ranking. Không cần FT khi: tìm kiếm chính xác có thể cover bằng index seek, LIKE với prefix (Name LIKE 'abc%' dùng index tốt), hoặc exact string matching.


Senior Level

Q9: Thiết kế audit solution cho GDPR compliance dùng Temporal Tables. Những gì cần xem xét?

Requirements GDPR:

  • Lưu lịch sử thay đổi (ai sửa gì, khi nào)
  • “Right to be forgotten”: có thể xóa data
  • Data không được giữ lâu hơn cần thiết

Solution với Temporal Tables:

  1. Temporal tables cho tất cả tables chứa PII
  2. Retention policy: để history lâu hơn operational data (ví dụ: operational 2 năm, history 7 năm cho compliance)
  3. “Right to be forgotten”: phức tạp với temporal — SYSTEM_VERSIONING = OFF để update/delete history, sau đó bật lại. Cân nhắc pseudonymization thay vì physical delete
  4. Separate history database để có thể backup/restore policy khác nhau
  5. Compression trên history table (ROW/PAGE COMPRESSION)
  6. Audit of who queried: Extended Events session capture selects trên sensitive tables

Q10: Giải thích In-Memory OLTP isolation levels. Tại sao không có traditional locking?

Memory-optimized tables dùng Optimistic Concurrency Control (OCC) thay vì pessimistic locking:

  • SNAPSHOT isolation (mặc định trong natively compiled procs): mỗi transaction thấy consistent snapshot tại thời điểm bắt đầu → không đọc uncommitted data, không block
  • REPEATABLE READ: thêm validation tại commit time — nếu row đã bị modify bởi concurrent transaction → commit fail, retry
  • SERIALIZABLE: full serializable validation

Nếu conflict tại commit: transaction phải retry (application cần handle 1205 hoặc 41302 errors).

Không có traditional locking vì: data structure dùng multi-version concurrency (Bw-Tree, linked lists), mỗi version immutable → readers không block writers, writers không block readers.

Q11: Query Store plan forcing — khi nào dùng và rủi ro là gì?

Khi dùng plan forcing:

  • Plan regression đột ngột sau SQL Server upgrade hoặc statistics update
  • Biết rõ plan cụ thể là optimal cho workload
  • Emergency fix trong khi tìm root cause thực sự

Cách dùng:

-- Tìm plan_id tốt từ Query Store history, sau đó force
EXEC sys.sp_query_store_force_plan @query_id = 42, @plan_id = 7;

Rủi ro:

  • Forced plan có thể không optimal khi data distribution thay đổi (ví dụ: tháng sau bảng có 10x rows)
  • Schema changes (thêm index mới, thay đổi table) làm forced plan invalid → tự fallback về optimizer choice
  • Che giấu vấn đề gốc (outdated stats, bad query design) thay vì fix

Best practice: dùng như giải pháp tạm thời, kèm theo ticket để fix root cause. Theo dõi performance sau khi force. Unforce plan sau khi fix xong.

Q12: Partition Switching + CDC — có vấn đề gì không?

Vấn đề: Partition SWITCH là metadata-only operation → CDC không capture data movement từ SWITCH. Nếu SWITCH partition từ staging vào production, CDC change table sẽ không có records cho các rows đó.

Giải pháp:

  1. Dùng SWITCH cho loading (bypass CDC capture) → sau SWITCH, apply CDC capture thủ công nếu cần replication
  2. Đặt CDC ở downstream (sau khi data đã fully loaded)
  3. Dùng Change Tracking thay vì CDC nếu chỉ cần row-level tracking (Change Tracking cũng không capture SWITCH)
  4. Implement custom audit logic cho partition switching operations

Partitioning - Phân vùng Bảng

Tại sao cần Partitioning?

Khi bảng có hàng chục triệu đến hàng tỷ rows (Very Large Tables), các vấn đề thường gặp:

  • Query performance chậm: full scan cả bảng dù chỉ cần 1 tháng data
  • Maintenance operations tốn thời gian: index rebuild/reorganize trên toàn bảng mất hàng giờ
  • Archiving phức tạp: xóa dữ liệu cũ bằng DELETE tốn kém, lock toàn bảng
  • Backup/restore chậm: không thể backup từng phần

Partitioning giải quyết:

  • Partition Elimination: optimizer chỉ scan partitions liên quan
  • Partition-level maintenance: rebuild index chỉ trên 1 partition
  • Fast archiving: ALTER TABLE...SWITCH — metadata-only operation, gần như instant
  • Filegroup management: phân vùng dữ liệu ra nhiều filegroup/disk

Lưu ý quan trọng: Partitioning KHÔNG thay thế indexes. Phải có proper indexes trên partitioned table.


Partition Function

Định nghĩa cách chia dữ liệu dựa trên một cột (partition key).

RANGE LEFT vs RANGE RIGHT

RANGE LEFTRANGE RIGHT
Boundary thuộc vềPartition bên trái (nhỏ hơn hoặc bằng)Partition bên phải (lớn hơn hoặc bằng)
Ví dụ boundary {2023}P1: ≤ 2023, P2: > 2023P1: < 2023, P2: ≥ 2023
Thường dùng vớiDate/time (boundary = end of period)Date/time (boundary = start of period)
-- RANGE RIGHT: boundary là đầu của period
-- P1: < 2022-01-01
-- P2: 2022-01-01 ≤ x < 2023-01-01
-- P3: 2023-01-01 ≤ x < 2024-01-01
-- P4: ≥ 2024-01-01
CREATE PARTITION FUNCTION pf_OrderDate (DATE)
AS RANGE RIGHT FOR VALUES (
    '2022-01-01',
    '2023-01-01',
    '2024-01-01'
);

-- RANGE LEFT: boundary là cuối của period
-- P1: ≤ 2021-12-31
-- P2: 2022-01-01 ≤ x ≤ 2022-12-31
-- P3: 2023-01-01 ≤ x ≤ 2023-12-31
-- P4: > 2023-12-31
CREATE PARTITION FUNCTION pf_OrderDate_Left (DATE)
AS RANGE LEFT FOR VALUES (
    '2021-12-31',
    '2022-12-31',
    '2023-12-31'
);

Kiểm tra Partition Function

-- Xem partition functions
SELECT * FROM sys.partition_functions;

-- Xem boundary values
SELECT 
    pf.name AS partition_function,
    pf.type_desc,
    prv.boundary_id,
    prv.value AS boundary_value
FROM sys.partition_functions pf
JOIN sys.partition_range_values prv ON pf.function_id = prv.function_id
ORDER BY pf.name, prv.boundary_id;

Partition Scheme

Map từng partition trong Partition Function sang một Filegroup cụ thể.

-- Dùng [PRIMARY] filegroup cho tất cả partitions (đơn giản nhất)
CREATE PARTITION SCHEME ps_OrderDate
AS PARTITION pf_OrderDate
ALL TO ([PRIMARY]);

-- Phân chia ra nhiều filegroups (best practice cho performance)
-- Thêm filegroups trước
ALTER DATABASE SalesDB ADD FILEGROUP FG_2022;
ALTER DATABASE SalesDB ADD FILEGROUP FG_2023;
ALTER DATABASE SalesDB ADD FILEGROUP FG_2024;
ALTER DATABASE SalesDB ADD FILEGROUP FG_Current;

ALTER DATABASE SalesDB ADD FILE (
    NAME = 'SalesDB_2022',
    FILENAME = 'D:\SQL\SalesDB_2022.ndf'
) TO FILEGROUP FG_2022;

-- Tạo partition scheme map sang filegroups
CREATE PARTITION SCHEME ps_OrderDate_MultiFG
AS PARTITION pf_OrderDate
TO (FG_2022, FG_2023, FG_2024, FG_Current);
-- Số filegroup = số partition + 1 (NEXT USED)

Partitioned Table

-- Tạo bảng partitioned - chỉ thêm ON [partition_scheme]([partition_key])
CREATE TABLE Sales.Orders (
    OrderID     INT NOT NULL,
    OrderDate   DATE NOT NULL,
    CustomerID  INT NOT NULL,
    TotalAmount DECIMAL(18, 2) NOT NULL,
    Status      VARCHAR(20) NOT NULL,
    CONSTRAINT PK_Orders PRIMARY KEY CLUSTERED (OrderID, OrderDate)
    -- Lưu ý: Partition key (OrderDate) phải trong clustered index key
)
ON ps_OrderDate (OrderDate);  -- Partition theo OrderDate

-- Kiểm tra phân phối dữ liệu theo partition
SELECT 
    p.partition_number,
    p.rows,
    prv.value AS boundary_value,
    fg.name AS filegroup_name
FROM sys.partitions p
JOIN sys.indexes i ON p.object_id = i.object_id AND p.index_id = i.index_id
JOIN sys.partition_schemes ps ON i.data_space_id = ps.data_space_id
JOIN sys.partition_functions pf ON ps.function_id = pf.function_id
LEFT JOIN sys.partition_range_values prv 
    ON pf.function_id = prv.function_id 
    AND prv.boundary_id = p.partition_number
LEFT JOIN sys.destination_data_spaces dds 
    ON ps.data_space_id = dds.partition_scheme_id 
    AND dds.destination_id = p.partition_number
JOIN sys.filegroups fg ON dds.data_space_id = fg.data_space_id
WHERE OBJECT_NAME(p.object_id) = 'Orders'
    AND i.index_id IN (0, 1)  -- Heap hoặc clustered index
ORDER BY p.partition_number;

Partitioned Index

Aligned vs Non-Aligned Index

AlignedNon-Aligned
Định nghĩaIndex dùng cùng partition scheme với tableIndex có partition scheme khác hoặc không partitioned
Partition switchCho phépKhông cho phép
MaintenanceRebuild từng partitionPhải rebuild toàn bộ
Khuyến nghịLuôn dùng alignedChỉ trong trường hợp đặc biệt
-- Aligned nonclustered index (partition key PHẢI có trong index key hoặc INCLUDE)
CREATE NONCLUSTERED INDEX IX_Orders_CustomerID
ON Sales.Orders (CustomerID, OrderDate)  -- OrderDate là partition key
ON ps_OrderDate (OrderDate);  -- Cùng partition scheme

-- Rebuild index chỉ một partition (aligned index)
ALTER INDEX IX_Orders_CustomerID ON Sales.Orders
REBUILD PARTITION = 3;  -- Chỉ rebuild partition 3 (năm 2023)

-- Rebuild tất cả partitions
ALTER INDEX IX_Orders_CustomerID ON Sales.Orders
REBUILD PARTITION = ALL;

Partition Elimination

Optimizer tự động bỏ qua các partitions không cần thiết dựa trên filter condition.

-- Query này sẽ chỉ scan partition của năm 2023
SELECT OrderID, CustomerID, TotalAmount
FROM Sales.Orders
WHERE OrderDate >= '2023-01-01' AND OrderDate < '2024-01-01';

-- Verify partition elimination qua execution plan
-- Trong execution plan: xem "Actual Partition Count" vs "Estimated Partition Count"

-- $PARTITION function: xác định row thuộc partition nào
SELECT 
    OrderID,
    OrderDate,
    $PARTITION.pf_OrderDate(OrderDate) AS PartitionNumber
FROM Sales.Orders
WHERE OrderID = 12345;

-- Đếm rows theo từng partition
SELECT 
    $PARTITION.pf_OrderDate(OrderDate) AS PartitionNumber,
    COUNT(*) AS RowCount
FROM Sales.Orders
GROUP BY $PARTITION.pf_OrderDate(OrderDate)
ORDER BY PartitionNumber;

Điều kiện để Partition Elimination hoạt động

  • WHERE clause phải filter trực tiếp trên partition key column
  • Không được dùng function bọc partition key: WHERE YEAR(OrderDate) = 2023không elimination; WHERE OrderDate >= '2023-01-01' AND OrderDate < '2024-01-01' elimination
  • Parameter sniffing có thể ảnh hưởng → dùng OPTION (OPTIMIZE FOR UNKNOWN) nếu cần

Partition Switching

Operation metadata-only (không di chuyển data vật lý) — cực nhanh (milliseconds dù table có tỷ rows).

Pattern: Loading dữ liệu mới (Switch IN)

-- 1. Tạo staging table với cùng schema, cùng partition scheme
CREATE TABLE Sales.Orders_Staging (
    OrderID     INT NOT NULL,
    OrderDate   DATE NOT NULL,
    CustomerID  INT NOT NULL,
    TotalAmount DECIMAL(18, 2) NOT NULL,
    Status      VARCHAR(20) NOT NULL,
    CONSTRAINT PK_Orders_Staging PRIMARY KEY CLUSTERED (OrderID, OrderDate)
)
ON ps_OrderDate (OrderDate);  -- Staging cũng phải partitioned

-- 2. Load dữ liệu vào staging (không lock production table)
INSERT INTO Sales.Orders_Staging
SELECT ... FROM ExternalSource;

-- 3. Build indexes trên staging
CREATE NONCLUSTERED INDEX IX_Staging_CustomerID
ON Sales.Orders_Staging (CustomerID, OrderDate)
ON ps_OrderDate (OrderDate);

-- 4. Switch IN: staging partition 4 → Orders partition 4 (instant!)
ALTER TABLE Sales.Orders_Staging 
SWITCH PARTITION 4 TO Sales.Orders PARTITION 4;

Pattern: Archiving dữ liệu cũ (Switch OUT)

-- 1. Tạo archive table (cùng schema, không cần partitioned)
CREATE TABLE Sales.Orders_Archive (
    OrderID     INT NOT NULL,
    OrderDate   DATE NOT NULL,
    CustomerID  INT NOT NULL,
    TotalAmount DECIMAL(18, 2) NOT NULL,
    Status      VARCHAR(20) NOT NULL,
    CONSTRAINT PK_Orders_Archive PRIMARY KEY CLUSTERED (OrderID, OrderDate)
)
ON FG_Archive;  -- Có thể là filegroup riêng, read-only

-- 2. Switch OUT: Orders partition 1 → archive table (instant!)
ALTER TABLE Sales.Orders 
SWITCH PARTITION 1 TO Sales.Orders_Archive;

-- 3. Archive table giờ có dữ liệu của partition 1
-- Có thể backup filegroup FG_2022 riêng lẻ, hoặc xóa data từ archive

Điều kiện cho Partition Switch

-- Kiểm tra điều kiện switch (sẽ báo lỗi nếu không thỏa)
-- 1. Source và target phải cùng schema (columns, data types, constraints)
-- 2. Aligned indexes phải tương đương
-- 3. Source partition phải là subset của target check constraints
-- 4. Nếu switch vào partitioned table: check constraint trên staging phải match
-- Thêm check constraint để SQL Server biết data chỉ thuộc partition đó
ALTER TABLE Sales.Orders_Staging
ADD CONSTRAINT CK_Staging_2024
CHECK (OrderDate >= '2024-01-01' AND OrderDate < '2025-01-01');

Split và Merge Partitions

Split: Thêm partition mới

-- Trước khi split: cần NEXT USED filegroup trong partition scheme
ALTER PARTITION SCHEME ps_OrderDate
NEXT USED FG_2025;

-- Split để thêm boundary cho năm 2025
ALTER PARTITION FUNCTION pf_OrderDate()
SPLIT RANGE ('2025-01-01');
-- Partition cũ bị chia làm 2: [2024-01-01, 2025-01-01) và [2025-01-01, ...)

Merge: Xóa partition (gộp 2 partitions lại)

-- Merge: gộp 2 partitions (dữ liệu sẽ nằm trong partition còn lại)
-- Điều kiện: partition bị merge phải EMPTY hoặc chấp nhận data move (chậm nếu có data)
ALTER PARTITION FUNCTION pf_OrderDate()
MERGE RANGE ('2022-01-01');
-- Boundary 2022-01-01 bị xóa, partition trước và sau gộp lại

Best Practice: Luôn SWITCH OUT data trước, rồi mới MERGE để tránh data movement.


Partition Maintenance - Sliding Window Pattern

Pattern phổ biến: thêm partition mới ở cuối, xóa partition cũ ở đầu.

-- Stored procedure: thêm partition mới cho tháng mới
CREATE OR ALTER PROCEDURE dbo.usp_AddNewMonthPartition
    @NewBoundary DATE  -- Ví dụ: '2025-01-01' cho tháng 1/2025
AS
BEGIN
    -- 1. Tạo filegroup và file mới
    DECLARE @FGName VARCHAR(50) = 'FG_' + CONVERT(VARCHAR, @NewBoundary, 112);
    DECLARE @SQL NVARCHAR(MAX);
    
    SET @SQL = N'ALTER DATABASE [SalesDB] ADD FILEGROUP [' + @FGName + N'];';
    EXEC sp_executesql @SQL;
    
    -- 2. Set NEXT USED
    SET @SQL = N'ALTER PARTITION SCHEME ps_OrderDate NEXT USED [' + @FGName + N'];';
    EXEC sp_executesql @SQL;
    
    -- 3. Split partition function
    SET @SQL = N'ALTER PARTITION FUNCTION pf_OrderDate() SPLIT RANGE (''' + 
               CONVERT(VARCHAR, @NewBoundary, 120) + N''');';
    EXEC sp_executesql @SQL;
    
    PRINT 'New partition added for boundary: ' + CONVERT(VARCHAR, @NewBoundary);
END;

Statistics per Partition

-- Cập nhật statistics cho từng partition riêng lẻ (SQL Server 2014+)
UPDATE STATISTICS Sales.Orders (IX_Orders_CustomerID)
WITH ROWCOUNT = 1000000, PAGECOUNT = 5000;  -- Không thực tế

-- Incremental statistics (SQL Server 2014+, Enterprise)
-- Chỉ update stats cho partitions có thay đổi
CREATE STATISTICS stats_OrderDate ON Sales.Orders(OrderDate)
WITH INCREMENTAL = ON;

-- Update chỉ partition 4
UPDATE STATISTICS Sales.Orders(stats_OrderDate)
WITH RESAMPLE ON PARTITIONS(4);

Khi nào KHÔNG nên dùng Partitioning

  • Bảng nhỏ (< 1 triệu rows): overhead nhiều hơn lợi ích
  • Thay thế index: partitioning có partition elimination nhưng không thay thế index seeks
  • OLTP đơn giản: queries trên nhiều partitions có thể chậm hơn nếu không có good indexes
  • Không có clear partition key: dùng date column thì phải có date trong WHERE clause
  • Development/Staging environments: tăng complexity không cần thiết

Horizontal vs Vertical Sharding (Khái niệm)

Horizontal Partitioning (sharding)Vertical Partitioning
Chia theoRows (theo giá trị partition key)Columns (chia bảng thành nhiều bảng)
SQL ServerTable Partitioning (cùng server) / Sharding (nhiều server)Bảng riêng, join lại khi cần
Ví dụOrders theo năm, Users theo regionTách BLOB columns ra bảng khác
Use caseScale out theo volume, data archivingReduce I/O cho hot columns

Q&A - Phỏng vấn Partitioning

Junior Level

Q1: Table partitioning trong SQL Server là gì?

Partitioning chia dữ liệu của một table thành các phần nhỏ hơn (partitions) dựa trên giá trị của một cột (partition key). Về mặt logical, vẫn là một table duy nhất; về mặt vật lý, dữ liệu được lưu trong các partition riêng biệt trên các filegroups.

Q2: Partition Function và Partition Scheme khác nhau như thế nào?

  • Partition Function: định nghĩa cách chia — boundary values và RANGE LEFT/RIGHT
  • Partition Scheme: định nghĩa lưu ở đâu — map từng partition sang filegroup

Q3: RANGE LEFT và RANGE RIGHT khác nhau như thế nào?

Quyết định boundary value thuộc về partition nào:

  • RANGE LEFT: boundary value thuộc partition bên trái (≤)
  • RANGE RIGHT: boundary value thuộc partition bên phải (≥)

Ví dụ: boundary 2024-01-01 với RANGE RIGHT → partition trước chứa < 2024-01-01, partition sau chứa ≥ 2024-01-01.


Mid Level

Q4: Partition Elimination là gì? Điều kiện để xảy ra?

Partition elimination là khi SQL Server optimizer bỏ qua các partitions không chứa dữ liệu thỏa mãn WHERE clause, giảm I/O đáng kể.

Điều kiện:

  • WHERE clause filter thẳng trên partition key column
  • Không wrap partition key trong function: YEAR(OrderDate) = 2023 không có elimination; OrderDate BETWEEN '2023-01-01' AND '2023-12-31' có elimination

Q5: Partition switching hoạt động thế nào? Tại sao lại nhanh?

ALTER TABLE...SWITCH là metadata-only operation: SQL Server chỉ cập nhật system catalog để thay đổi “ownership” của partition, không di chuyển data pages vật lý. Vì vậy chỉ mất milliseconds dù table có tỷ rows.

Điều kiện: source và target phải cùng schema, indexes, constraints phải compatible, data phải thực sự chỉ thuộc partition target (enforce bằng CHECK constraint).

Q6: Tại sao phải có partition key trong clustered index?

Partition key phải là một phần của clustered index key để đảm bảo aligned partitioning — data rows thuộc partition nào thì index rows cũng nằm trên cùng partition đó. Nếu không aligned, partition switching sẽ không hoạt động.


Senior Level

Q7: Thiết kế sliding window pattern cho bảng Orders 5 năm dữ liệu, archive hàng tháng?

Pattern:

  1. Partition function: boundary hàng tháng (60+ boundaries cho 5 năm)
  2. Mỗi tháng: tạo partition mới cho tháng tới (SPLIT RANGE với NEXT USED filegroup)
  3. Archive tháng cũ:
    • Chuẩn bị staging/archive table với correct check constraint
    • SWITCH OUT partition cũ nhất vào archive table (instant)
    • MERGE RANGE để gộp empty partition (không còn data)
    • Backup filegroup cũ, đánh read_only
  4. Automate bằng SQL Agent job chạy đầu tháng

Q8: Incremental statistics khác gì với statistics thông thường trên partitioned table?

Statistics thông thường: khi data thay đổi ở bất kỳ partition nào, cần update statistics cho toàn bộ index → tốn kém với bảng lớn.

Incremental statistics (SQL Server 2014+, Enterprise): statistics được maintain per-partition. Khi UPDATE STATISTICS, chỉ cần update partitions có thay đổi. Với sliding window pattern (chỉ partition mới nhất có writes), chỉ cần update 1 partition thay vì toàn bộ bảng.

Q9: Aligned vs non-aligned index — khi nào dùng non-aligned?

Non-aligned index hiếm khi cần thiết. Có thể dùng khi:

  • Query patterns cần access data across partitions dựa trên column khác (không phải partition key)
  • Không cần partition switch cho index này
  • Biết rõ query sẽ range scan across partitions và global index nhanh hơn

Nhưng nhược điểm lớn: không switch được partition nếu bảng có non-aligned index → phải drop index trước, switch, rồi create lại → mất nhiều thời gian và ảnh hưởng hiệu năng.

JSON & XML trong SQL Server

JSON trong SQL Server (2016+)

Lưu trữ JSON

SQL Server không có kiểu dữ liệu JSON riêng — JSON được lưu dưới dạng NVARCHAR(MAX).

-- Tạo bảng với cột JSON
CREATE TABLE Products (
    ProductID   INT PRIMARY KEY IDENTITY,
    Name        NVARCHAR(200) NOT NULL,
    Attributes  NVARCHAR(MAX),  -- JSON column
    CONSTRAINT CK_Products_Attributes CHECK (ISJSON(Attributes) = 1)
);

-- Insert dữ liệu JSON
INSERT INTO Products (Name, Attributes) VALUES 
('Laptop Dell XPS', N'{
    "brand": "Dell",
    "model": "XPS 15",
    "specs": {
        "cpu": "Intel Core i7-12700H",
        "ram": 32,
        "storage": 512
    },
    "tags": ["gaming", "professional", "lightweight"],
    "inStock": true
}');

-- ISJSON: kiểm tra chuỗi có phải JSON hợp lệ không
SELECT ISJSON('{"key": "value"}');  -- 1 (TRUE)
SELECT ISJSON('invalid json');       -- 0 (FALSE)
SELECT ISJSON(NULL);                  -- NULL

FOR JSON: Xuất dữ liệu dạng JSON

FOR JSON PATH

Cho phép control cấu trúc JSON bằng cách đặt tên cột theo dạng object.property.

-- FOR JSON PATH cơ bản
SELECT 
    ProductID AS 'id',
    Name AS 'name',
    JSON_VALUE(Attributes, '$.brand') AS 'brand'
FROM Products
FOR JSON PATH;
-- Output: [{"id":1,"name":"Laptop Dell XPS","brand":"Dell"}]

-- WITH ROOT: bọc array trong một object với key
SELECT ProductID, Name
FROM Products
FOR JSON PATH, ROOT('products');
-- Output: {"products":[{"ProductID":1,"Name":"Laptop Dell XPS"}]}

-- WITHOUT_ARRAY_WRAPPER: khi chỉ có 1 row, không bọc trong array
SELECT TOP 1 ProductID, Name
FROM Products
FOR JSON PATH, WITHOUT_ARRAY_WRAPPER;
-- Output: {"ProductID":1,"Name":"Laptop Dell XPS"}

-- Nested objects (dùng tên cột có dấu chấm)
SELECT 
    o.OrderID AS 'order.id',
    o.OrderDate AS 'order.date',
    c.CustomerName AS 'order.customer.name',
    c.Email AS 'order.customer.email'
FROM Orders o
JOIN Customers c ON o.CustomerID = c.CustomerID
FOR JSON PATH;
-- Output: [{"order":{"id":1,"date":"2026-04-01","customer":{"name":"Nguyen Van A","email":"a@b.com"}}}]

FOR JSON AUTO

Tự động tạo cấu trúc JSON dựa trên thứ tự SELECT và JOIN.

-- FOR JSON AUTO: cấu trúc tự động từ table alias
SELECT 
    o.OrderID,
    o.OrderDate,
    c.CustomerName,
    c.Email
FROM Orders o
JOIN Customers c ON o.CustomerID = c.CustomerID
FOR JSON AUTO;
-- Output: [{"OrderID":1,"OrderDate":"2026-04-01","c":[{"CustomerName":"Nguyen Van A"}]}]

Khuyến nghị: Dùng FOR JSON PATH để có control tốt hơn về cấu trúc output.


OPENJSON: Parse JSON thành Rows

OPENJSON cơ bản (không có schema)

DECLARE @json NVARCHAR(MAX) = N'[
    {"id": 1, "name": "Product A", "price": 100.00},
    {"id": 2, "name": "Product B", "price": 200.00}
]';

-- Trả về key, value, type cho mỗi element
SELECT * FROM OPENJSON(@json);
-- key | value                                | type
-- 0   | {"id":1,"name":"Product A",...}      | 5 (object)
-- 1   | {"id":2,"name":"Product B",...}      | 5 (object)

-- Một level sâu hơn
SELECT * FROM OPENJSON(@json, '$[0]');
-- key   | value    | type
-- id    | 1        | 2 (number)
-- name  | Product A| 1 (string)
-- price | 100.00   | 2 (number)

OPENJSON với WITH clause (có schema)

-- WITH clause định nghĩa output schema
DECLARE @json NVARCHAR(MAX) = N'[
    {"id": 1, "name": "Product A", "price": 100.00, "active": true},
    {"id": 2, "name": "Product B", "price": 200.00, "active": false}
]';

SELECT *
FROM OPENJSON(@json)
WITH (
    ProductID   INT         '$.id',
    ProductName NVARCHAR(100) '$.name',
    Price       DECIMAL(10,2) '$.price',
    IsActive    BIT         '$.active'
);

-- Join với bảng thực thế
SELECT p.Name, oj.Price
FROM Products p
CROSS APPLY OPENJSON(p.Attributes)
WITH (
    Brand   NVARCHAR(100) '$.brand',
    RAM     INT           '$.specs.ram',
    Storage INT           '$.specs.storage',
    Tags    NVARCHAR(MAX) '$.tags' AS JSON  -- AS JSON để lấy nested array/object
) oj;

-- Parse nested array
DECLARE @order NVARCHAR(MAX) = N'{
    "orderId": 1001,
    "items": [
        {"productId": 1, "qty": 2, "price": 50.00},
        {"productId": 2, "qty": 1, "price": 150.00}
    ]
}';

SELECT 
    JSON_VALUE(@order, '$.orderId') AS OrderID,
    items.ProductID,
    items.Qty,
    items.Price
FROM OPENJSON(@order, '$.items')
WITH (
    ProductID INT '$.productId',
    Qty       INT '$.qty',
    Price     DECIMAL(10,2) '$.price'
) items;

JSON Functions

JSON_VALUE: Lấy giá trị scalar

-- Cú pháp: JSON_VALUE(expression, path)
-- Trả về: NVARCHAR(4000), NULL nếu path không tìm thấy

SELECT 
    JSON_VALUE(Attributes, '$.brand') AS Brand,
    JSON_VALUE(Attributes, '$.specs.ram') AS RAM,
    JSON_VALUE(Attributes, '$.tags[0]') AS FirstTag  -- Array index
FROM Products;

-- Strict mode: ném lỗi nếu path không tồn tại hoặc giá trị không phải scalar
SELECT JSON_VALUE(Attributes, 'strict $.brand') AS Brand FROM Products;

-- Lax mode (mặc định): trả về NULL nếu không tìm thấy
SELECT JSON_VALUE(Attributes, 'lax $.nonexistent') AS Val FROM Products;  -- NULL

JSON_QUERY: Lấy object/array

-- Cú pháp: JSON_QUERY(expression, path)
-- Trả về: NVARCHAR(MAX) chứa JSON object hoặc array

SELECT 
    JSON_QUERY(Attributes, '$.specs') AS Specs,   -- Trả về {"cpu":"...","ram":32}
    JSON_QUERY(Attributes, '$.tags') AS Tags       -- Trả về ["gaming","professional"]
FROM Products;

-- Kết hợp JSON_VALUE và JSON_QUERY trong một query
SELECT 
    Name,
    JSON_VALUE(Attributes, '$.brand') AS Brand,
    JSON_QUERY(Attributes, '$.specs') AS Specs,
    JSON_VALUE(Attributes, '$.specs.ram') AS RAM
FROM Products;

JSON_MODIFY: Cập nhật JSON

-- JSON_MODIFY: cập nhật giá trị trong JSON (trả về JSON mới, không modify in-place)
DECLARE @json NVARCHAR(MAX) = N'{"brand": "Dell", "specs": {"ram": 16}}';

-- Cập nhật giá trị hiện có
SELECT JSON_MODIFY(@json, '$.specs.ram', 32);
-- Output: {"brand":"Dell","specs":{"ram":32}}

-- Thêm property mới
SELECT JSON_MODIFY(@json, '$.color', 'Silver');
-- Output: {"brand":"Dell","specs":{"ram":16},"color":"Silver"}

-- Xóa property (set = NULL)
SELECT JSON_MODIFY(@json, '$.brand', NULL);
-- Output: {"specs":{"ram":16}}

-- Append vào array
SELECT JSON_MODIFY(@json, 'append $.tags', 'sale');

-- UPDATE thực tế trong database
UPDATE Products
SET Attributes = JSON_MODIFY(Attributes, '$.specs.ram', 64)
WHERE ProductID = 1;

-- Update nhiều properties
UPDATE Products
SET Attributes = JSON_MODIFY(
                    JSON_MODIFY(Attributes, '$.specs.ram', 64),
                    '$.specs.storage', 1024)
WHERE ProductID = 1;

JSON Index Strategy

SQL Server không có native JSON index. Giải pháp: computed column + index.

-- Tạo computed column từ JSON path
ALTER TABLE Products
ADD Brand AS JSON_VALUE(Attributes, '$.brand') PERSISTED;

ALTER TABLE Products
ADD RAM AS CAST(JSON_VALUE(Attributes, '$.specs.ram') AS INT) PERSISTED;

-- Tạo index trên computed column
CREATE INDEX IX_Products_Brand ON Products(Brand);
CREATE INDEX IX_Products_RAM ON Products(RAM);

-- Query sẽ tự động dùng index
SELECT * FROM Products WHERE Brand = 'Dell';  -- Index seek!
SELECT * FROM Products WHERE RAM >= 16;       -- Index seek!

-- Verify với execution plan
-- Nếu không dùng PERSISTED: SQL Server vẫn có thể dùng index nhưng phải compute on-the-fly

XML trong SQL Server

XML data type

-- Typed XML: validated against XML schema collection
-- Untyped XML: không validate schema
CREATE TABLE Documents (
    DocID       INT PRIMARY KEY IDENTITY,
    Title       NVARCHAR(200),
    Content     XML,                    -- Untyped
    TypedContent XML(SchemaCollection1) -- Typed (nếu có schema collection)
);

-- Giới hạn: XML column tối đa 2GB
-- XML type tự động parse và lưu dưới dạng binary (không phải text thuần)

XML Schema Collections

-- Tạo XML schema collection
CREATE XML SCHEMA COLLECTION ProductSchema AS N'
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="Product">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="Name" type="xs:string"/>
                <xs:element name="Price" type="xs:decimal"/>
                <xs:element name="InStock" type="xs:boolean"/>
            </xs:sequence>
            <xs:attribute name="ID" type="xs:integer" use="required"/>
        </xs:complexType>
    </xs:element>
</xs:schema>';

-- Dùng schema collection trong column definition
CREATE TABLE TypedProducts (
    ID      INT PRIMARY KEY,
    Data    XML(ProductSchema)  -- Validated against ProductSchema
);

-- Insert: sẽ validate theo schema
INSERT INTO TypedProducts VALUES (1, 
    N'<Product ID="1"><Name>Laptop</Name><Price>999.99</Price><InStock>true</InStock></Product>');

FOR XML: Xuất dữ liệu dạng XML

FOR XML RAW

-- Mỗi row → một element <row>
SELECT ProductID, Name
FROM Products
FOR XML RAW;
-- Output: <row ProductID="1" Name="Laptop Dell XPS"/>

-- Custom element name
SELECT ProductID, Name
FROM Products
FOR XML RAW('Product');
-- Output: <Product ProductID="1" Name="Laptop Dell XPS"/>

-- ELEMENTS: attributes → sub-elements
SELECT ProductID, Name
FROM Products
FOR XML RAW('Product'), ELEMENTS;
-- Output: <Product><ProductID>1</ProductID><Name>Laptop Dell XPS</Name></Product>

FOR XML AUTO

-- Tự động tạo hierarchy từ table name
SELECT o.OrderID, o.OrderDate, c.CustomerName
FROM Orders o
JOIN Customers c ON o.CustomerID = c.CustomerID
FOR XML AUTO, ELEMENTS;

FOR XML PATH (linh hoạt nhất)

-- Full control qua column aliases (XPath)
SELECT 
    ProductID AS '@ID',           -- Attribute
    Name AS 'Name',               -- Child element
    JSON_VALUE(Attributes, '$.brand') AS 'Details/Brand',  -- Nested element
    JSON_VALUE(Attributes, '$.specs.ram') AS 'Details/RAM'
FROM Products
FOR XML PATH('Product'), ROOT('Products');
-- Output:
-- <Products>
--   <Product ID="1">
--     <Name>Laptop Dell XPS</Name>
--     <Details><Brand>Dell</Brand><RAM>32</RAM></Details>
--   </Product>
-- </Products>

-- Build comma-separated list (classic trick với FOR XML PATH)
SELECT STUFF(
    (SELECT ', ' + Name 
     FROM Products
     FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'),
    1, 2, '') AS ProductList;

XML Methods

.value() - Lấy giá trị scalar

DECLARE @xml XML = N'<Product ID="1">
    <Name>Laptop</Name>
    <Price>999.99</Price>
    <Specs><RAM>32</RAM><Storage>512</Storage></Specs>
</Product>';

-- .value(XQuery, SQL_type)
SELECT 
    @xml.value('(/Product/@ID)[1]', 'INT') AS ProductID,
    @xml.value('(/Product/Name)[1]', 'NVARCHAR(100)') AS Name,
    @xml.value('(/Product/Price)[1]', 'DECIMAL(10,2)') AS Price,
    @xml.value('(/Product/Specs/RAM)[1]', 'INT') AS RAM;
-- Lưu ý: [1] là bắt buộc (lấy phần tử đầu tiên)

.query() - Lấy XML fragment

-- .query(XQuery): trả về XML
SELECT @xml.query('/Product/Specs') AS Specs;
-- Output: <Specs><RAM>32</RAM><Storage>512</Storage></Specs>

SELECT @xml.query('for $p in /Product return $p/Name') AS Names;

.nodes() - Shred XML thành rows

DECLARE @xml XML = N'<Products>
    <Product ID="1"><Name>Laptop</Name><Price>999.99</Price></Product>
    <Product ID="2"><Name>Mouse</Name><Price>29.99</Price></Product>
    <Product ID="3"><Name>Keyboard</Name><Price>49.99</Price></Product>
</Products>';

-- .nodes(): trả về bảng của XML nodes
SELECT 
    p.value('@ID', 'INT') AS ProductID,
    p.value('(Name)[1]', 'NVARCHAR(100)') AS Name,
    p.value('(Price)[1]', 'DECIMAL(10,2)') AS Price
FROM @xml.nodes('/Products/Product') AS T(p);
-- Kết quả: 3 rows như một bảng thông thường

-- Áp dụng với column trong bảng
SELECT 
    d.DocID,
    p.value('@ID', 'INT') AS ProductID,
    p.value('(Name)[1]', 'NVARCHAR(100)') AS Name
FROM Documents d
CROSS APPLY d.Content.nodes('/Products/Product') AS T(p);

.exist() - Kiểm tra sự tồn tại

-- .exist(): trả về 1 (TRUE) hoặc 0 (FALSE)
SELECT 
    @xml.exist('/Product/Specs/RAM') AS HasRAM,           -- 1
    @xml.exist('/Product/Specs/GPU') AS HasGPU,           -- 0
    @xml.exist('/Product[@ID="1"]') AS IDIs1,              -- 1
    @xml.exist('/Product/Price[. > 500]') AS PriceOver500  -- 1

-- Dùng trong WHERE clause
SELECT DocID, Title
FROM Documents
WHERE Content.exist('/Products/Product[@ID="1"]') = 1;

.modify() - Cập nhật XML

DECLARE @xml XML = N'<Product><Name>Laptop</Name><Price>999</Price></Product>';

-- Insert: thêm node mới
SET @xml.modify('insert <Color>Silver</Color> into (/Product)[1]');

-- Update: thay đổi giá trị
SET @xml.modify('replace value of (/Product/Price/text())[1] with 1099');

-- Delete: xóa node
SET @xml.modify('delete /Product/Color');

SELECT @xml;

XML Indexes

-- Primary XML Index: bắt buộc có trước khi tạo secondary
CREATE PRIMARY XML INDEX PXML_Documents_Content
ON Documents(Content);

-- Secondary XML indexes (chọn loại phù hợp với query pattern)
-- PATH: tối ưu cho queries dùng path expressions trong .exist() và .nodes()
CREATE XML INDEX IXML_Documents_Content_PATH
ON Documents(Content)
USING XML INDEX PXML_Documents_Content
FOR PATH;

-- VALUE: tối ưu cho queries dùng .value() để tìm kiếm
CREATE XML INDEX IXML_Documents_Content_VALUE
ON Documents(Content)
USING XML INDEX PXML_Documents_Content
FOR VALUE;

-- PROPERTY: tối ưu khi retrieve nhiều properties từ cùng một node
CREATE XML INDEX IXML_Documents_Content_PROPERTY
ON Documents(Content)
USING XML INDEX PXML_Documents_Content
FOR PROPERTY;

OPENXML Function (Legacy)

-- OPENXML: cách cũ để shred XML (trước SQL 2005)
-- Khuyến nghị: dùng .nodes() thay thế cho code mới

DECLARE @xml NVARCHAR(MAX) = N'<Products>
    <Product ID="1"><Name>Laptop</Name><Price>999.99</Price></Product>
    <Product ID="2"><Name>Mouse</Name><Price>29.99</Price></Product>
</Products>';

DECLARE @handle INT;

-- Chuẩn bị XML document trong memory
EXEC sp_xml_preparedocument @handle OUTPUT, @xml;

-- Query với OPENXML
SELECT *
FROM OPENXML(@handle, '/Products/Product', 2)  -- 2 = element-centric
WITH (
    ProductID   INT         '@ID',
    Name        VARCHAR(100) 'Name',
    Price       DECIMAL(10,2) 'Price'
);

-- QUAN TRỌNG: phải giải phóng memory
EXEC sp_xml_removedocument @handle;

Q&A - Phỏng vấn JSON & XML

Junior Level

Q1: Tại sao SQL Server không có kiểu dữ liệu JSON riêng? Lưu JSON như thế nào?

SQL Server lưu JSON trong NVARCHAR(MAX). Dùng constraint CHECK (ISJSON(column) = 1) để đảm bảo luôn là JSON hợp lệ. Điều này cho phép backward compatibility và flexibility — JSON vẫn là text, SQL Server cung cấp các hàm để làm việc với nó.

Q2: JSON_VALUEJSON_QUERY khác nhau thế nào?

  • JSON_VALUE: trả về scalar value (string, number, boolean) → NVARCHAR(4000)
  • JSON_QUERY: trả về object hoặc arrayNVARCHAR(MAX) chứa JSON

Dùng nhầm sẽ trả về NULL: JSON_VALUE trả về NULL nếu path là object/array; JSON_QUERY trả về NULL nếu path là scalar.

Q3: FOR JSON PATH và FOR JSON AUTO khác nhau thế nào?

  • FOR JSON AUTO: cấu trúc JSON tự động từ table names và query structure — ít control
  • FOR JSON PATH: dùng tên cột với dấu chấm ('order.customer.name') để define nested structure → flexible hơn, khuyến khích dùng

Mid Level

Q4: Làm thế nào để index cho JSON column? Tại sao cần vậy?

JSON column không có native index. Giải pháp: tạo computed column với JSON_VALUE(column, '$.path') rồi đánh PERSISTED, sau đó tạo index trên computed column đó.

Ưu điểm: query WHERE ComputedCol = 'value' dùng index seek thay vì full scan toàn bảng.

Q5: .value(), .query(), .nodes(), .exist(), .modify() — mỗi method dùng khi nào?

  • .value(path, type): lấy scalar value (string, number) từ XML
  • .query(xquery): lấy XML fragment (object, collection)
  • .nodes(path): “shred” XML thành rows — dùng với CROSS/OUTER APPLY
  • .exist(xquery): kiểm tra path có tồn tại không (WHERE clause)
  • .modify(dml): cập nhật XML in-place (INSERT, DELETE, REPLACE)

Q6: Tại sao nên dùng .nodes() thay vì OPENXML?

.nodes() là phương pháp native, không cần quản lý memory, không cần sp_xml_preparedocument/sp_xml_removedocument. Nếu quên gọi sp_xml_removedocument, memory leak xảy ra. .nodes() cũng được optimize tốt hơn bởi query optimizer.


Senior Level

Q7: Khi nào nên lưu dữ liệu dạng JSON trong SQL Server thay vì normalized tables?

Phù hợp lưu JSON khi:

  • Schema thay đổi thường xuyên (attributes, metadata) — không muốn migration mỗi lần thêm field
  • Sparse attributes — nhiều loại sản phẩm với thuộc tính khác nhau (EAV pattern replacement)
  • Semi-structured data — external API responses, event payloads
  • Read-heavy, write-light — JSON stored proc → ít joins hơn

Không nên lưu JSON khi:

  • Cần query, filter, aggregate trên các fields bên trong JSON thường xuyên → dùng computed columns + index nhưng đây là workaround
  • Foreign key relationships → JSON không enforce referential integrity
  • Data integrity quan trọng → không có constraint enforcement trong JSON values

Q8: Thiết kế schema cho hệ thống lưu trữ product catalog với attributes khác nhau per category?

Option 1: JSON column

CREATE TABLE Products (
    ProductID INT PRIMARY KEY,
    CategoryID INT REFERENCES Categories(ID),
    Name NVARCHAR(200),
    Attributes NVARCHAR(MAX),  -- JSON, schema per category
    CONSTRAINT CK_Attributes CHECK (ISJSON(Attributes) = 1)
);
-- Thêm computed columns + indexes cho các attributes thường query

Option 2: Per-category tables (schema-first)

Option 3: EAV (Entity-Attribute-Value) — generally avoid

Khuyến nghị: Option 1 (JSON) cho trường hợp nhiều categories với attributes đa dạng. Dùng computed columns + indexes cho top N queryable attributes. Validate JSON structure ở application layer hoặc dùng CHECK constraints.

Q9: XML Primary XML Index vs Secondary XML Indexes — giải thích mục đích từng loại?

Primary XML Index: shreds XML column thành bảng nội bộ chứa tất cả tags, values, paths. Tất cả secondary indexes đều phụ thuộc vào primary.

Secondary XML Indexes:

  • PATH: B-tree trên đường dẫn → tối ưu cho WHERE col.exist('/a/b/c') = 1
  • VALUE: B-tree trên values → tối ưu cho WHERE col.value('...', 'INT') = 42
  • PROPERTY: B-tree trên (path, value) pairs → tối ưu cho retrieve nhiều properties từ same node

Trong thực tế: nếu query chủ yếu filter bằng .exist(), dùng PATH index. Nếu filter bằng .value(), dùng VALUE index. Nếu dùng cả hai → có thể cần cả hai secondary indexes.

Temporal Tables

Định nghĩa

Temporal Tables (còn gọi là system-versioned temporal tables) là tính năng được giới thiệu trong SQL Server 2016, theo chuẩn SQL:2011. Temporal tables tự động lưu trữ toàn bộ lịch sử thay đổi của dữ liệu vào một history table riêng, cho phép query dữ liệu tại bất kỳ thời điểm nào trong quá khứ.

Cách hoạt động

  • Mỗi row trong temporal table có 2 datetime2 columns xác định khoảng thời gian hiệu lực: ValidFromValidTo
  • Khi UPDATE hoặc DELETE: SQL Server tự động ghi bản cũ vào history table với ValidTo = current_time
  • Khi INSERT: ValidFrom = current_time, ValidTo = '9999-12-31 23:59:59.9999999' (max datetime2)
  • Không cần trigger — hoàn toàn tự động và atomic

Tạo Temporal Table

Tạo từ đầu

-- Tạo temporal table với explicit history table name
CREATE TABLE dbo.Employees (
    EmployeeID      INT NOT NULL PRIMARY KEY,
    Name            NVARCHAR(100) NOT NULL,
    Department      NVARCHAR(50) NOT NULL,
    Salary          DECIMAL(15, 2) NOT NULL,
    ManagerID       INT NULL,
    -- Period columns: MUST be datetime2, NOT NULL
    ValidFrom       DATETIME2 GENERATED ALWAYS AS ROW START NOT NULL,
    ValidTo         DATETIME2 GENERATED ALWAYS AS ROW END NOT NULL,
    -- Declare period
    PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo)
)
WITH (
    SYSTEM_VERSIONING = ON (
        HISTORY_TABLE = dbo.Employees_History,
        DATA_CONSISTENCY_CHECK = ON  -- Kiểm tra tính nhất quán khi enable
    )
);

-- Tạo với auto-generated hidden period columns (ẩn khỏi SELECT *)
CREATE TABLE dbo.Products (
    ProductID       INT NOT NULL PRIMARY KEY,
    ProductName     NVARCHAR(100) NOT NULL,
    Price           DECIMAL(10, 2) NOT NULL,
    IsActive        BIT NOT NULL DEFAULT 1,
    -- Hidden period columns (không xuất hiện trong SELECT *)
    ValidFrom       DATETIME2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL,
    ValidTo         DATETIME2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL,
    PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo)
)
WITH (SYSTEM_VERSIONING = ON);
-- SQL Server tự tạo history table với tên MSSQL_TemporalHistoryFor_<object_id>

Index mặc định của History Table

SQL Server tự động tạo một clustered index trên history table dựa trên period columns:

-- History table structure được tạo tự động:
-- dbo.Employees_History với clustered index trên (ValidFrom, ValidTo)
-- Không có PRIMARY KEY constraint

-- Xem history table
SELECT * FROM dbo.Employees_History ORDER BY ValidFrom;

-- Kiểm tra indexes trên history table
SELECT i.name, i.type_desc, STRING_AGG(c.name, ', ') AS key_columns
FROM sys.indexes i
JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
WHERE i.object_id = OBJECT_ID('dbo.Employees_History')
GROUP BY i.name, i.type_desc;

Convert Existing Table thành Temporal Table

-- Bước 1: Thêm period columns vào table hiện có
ALTER TABLE dbo.Customers
ADD ValidFrom DATETIME2 GENERATED ALWAYS AS ROW START NOT NULL
    CONSTRAINT DF_Customers_ValidFrom DEFAULT SYSUTCDATETIME(),
    ValidTo DATETIME2 GENERATED ALWAYS AS ROW END NOT NULL
    CONSTRAINT DF_Customers_ValidTo DEFAULT CONVERT(DATETIME2, '9999-12-31 23:59:59.9999999'),
    PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo);

-- Bước 2: Enable system versioning
ALTER TABLE dbo.Customers
SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.Customers_History));

-- Kiểm tra kết quả
SELECT 
    name,
    temporal_type_desc,
    history_table_id,
    OBJECT_NAME(history_table_id) AS history_table_name
FROM sys.tables
WHERE name = 'Customers';

Querying Historical Data

FOR SYSTEM_TIME AS OF

Trả về rows có hiệu lực tại thời điểm cụ thể (ValidFrom <= t AND ValidTo > t):

-- Dữ liệu tại thời điểm cụ thể
SELECT EmployeeID, Name, Department, Salary, ValidFrom, ValidTo
FROM dbo.Employees
FOR SYSTEM_TIME AS OF '2023-06-01 00:00:00';

-- Dùng với biến
DECLARE @asOf DATETIME2 = DATEADD(MONTH, -3, SYSUTCDATETIME());
SELECT * FROM dbo.Employees FOR SYSTEM_TIME AS OF @asOf;

FOR SYSTEM_TIME FROM…TO

Trả về rows có any overlap với khoảng thời gian [start, end) — không bao gồm end:

-- Rows có hiệu lực bất kỳ lúc nào trong khoảng 2023 Q1
SELECT EmployeeID, Name, Salary, ValidFrom, ValidTo
FROM dbo.Employees
FOR SYSTEM_TIME FROM '2023-01-01' TO '2023-04-01'
ORDER BY EmployeeID, ValidFrom;
-- Điều kiện: ValidFrom < '2023-04-01' AND ValidTo > '2023-01-01'

FOR SYSTEM_TIME BETWEEN…AND

Tương tự FROM…TO nhưng bao gồm end boundary (inclusive):

-- BETWEEN là inclusive cả 2 đầu
SELECT EmployeeID, Name, ValidFrom, ValidTo
FROM dbo.Employees
FOR SYSTEM_TIME BETWEEN '2023-01-01' AND '2023-12-31 23:59:59.9999999';
-- Điều kiện: ValidFrom <= '2023-12-31...' AND ValidTo > '2023-01-01'

FOR SYSTEM_TIME CONTAINED IN

Chỉ trả về rows hoàn toàn nằm trong khoảng thời gian (ValidFrom >= start AND ValidTo <= end):

-- Rows tồn tại trong DB hoàn toàn trong khoảng thời gian này
-- Hữu ích để tìm rows đã bị xóa trong period
SELECT EmployeeID, Name, ValidFrom, ValidTo
FROM dbo.Employees
FOR SYSTEM_TIME CONTAINED IN ('2023-01-01', '2024-01-01');
-- Điều kiện: ValidFrom >= '2023-01-01' AND ValidTo <= '2024-01-01'

FOR SYSTEM_TIME ALL

Trả về tất cả rows từ cả current và history table:

-- Toàn bộ lịch sử của tất cả employees
SELECT EmployeeID, Name, Department, Salary, ValidFrom, ValidTo
FROM dbo.Employees
FOR SYSTEM_TIME ALL
ORDER BY EmployeeID, ValidFrom;

-- Xem lịch sử thay đổi lương của một employee
SELECT EmployeeID, Name, Salary, ValidFrom, ValidTo
FROM dbo.Employees
FOR SYSTEM_TIME ALL
WHERE EmployeeID = 42
ORDER BY ValidFrom;

Các Use Cases phổ biến

Audit Trail

-- Xem mọi thay đổi của một row
SELECT 
    EmployeeID,
    Name,
    Department,
    Salary,
    CASE 
        WHEN ValidTo = '9999-12-31 23:59:59.9999999' THEN 'CURRENT'
        ELSE 'HISTORICAL'
    END AS row_status,
    ValidFrom AS changed_at,
    ValidTo AS valid_until
FROM dbo.Employees
FOR SYSTEM_TIME ALL
WHERE EmployeeID = 42
ORDER BY ValidFrom;

Point-in-Time Reporting

-- Financial report tại thời điểm cuối tháng trước
DECLARE @reportDate DATETIME2 = DATEADD(SECOND, -1, 
    DATEFROMPARTS(YEAR(GETDATE()), MONTH(GETDATE()), 1));

SELECT 
    e.EmployeeID,
    e.Name,
    e.Department,
    e.Salary
FROM dbo.Employees FOR SYSTEM_TIME AS OF @reportDate e
WHERE e.Department = 'Engineering'
ORDER BY e.Name;

Slowly Changing Dimensions (SCD Type 2)

-- Temporal table thay thế cho SCD Type 2 manual implementation
-- Không cần EffectiveDate, ExpirationDate, IsCurrent columns thủ công

-- Xem dimension values tại thời điểm cụ thể để join với fact table
SELECT 
    f.SaleAmount,
    f.SaleDate,
    p.ProductName,
    p.Price AS price_at_sale_time
FROM dbo.SalesFact f
JOIN dbo.Products FOR SYSTEM_TIME AS OF f.SaleDate p 
    ON f.ProductID = p.ProductID;

History Table

Cấu trúc và Indexes

-- History table không có:
-- - PRIMARY KEY
-- - UNIQUE constraints
-- - FOREIGN KEY constraints
-- - Triggers
-- - DEFAULT constraints

-- Chỉ có 1 clustered index tự động tạo trên (ValidTo, ValidFrom)
-- HOẶC (ValidFrom, ValidTo) tùy SQL Server version

-- Thêm index vào history table để optimize historical queries
CREATE INDEX IX_Employees_History_EmployeeID_Period
ON dbo.Employees_History (EmployeeID, ValidFrom, ValidTo);

-- Xem dữ liệu trong history table trực tiếp
SELECT * FROM dbo.Employees_History WHERE EmployeeID = 42;
-- Lưu ý: Không có syntax FOR SYSTEM_TIME khi query history table trực tiếp

Data Retention cho History Table

-- SQL Server 2017+: Cấu hình retention period để tự động cleanup history
ALTER TABLE dbo.Employees
SET (SYSTEM_VERSIONING = ON (
    HISTORY_RETENTION_PERIOD = 1 YEAR  -- Giữ 1 năm lịch sử
));
-- Các options: INFINITE (default), DAYS, WEEKS, MONTHS, YEARS

-- Xem cấu hình retention
SELECT 
    name,
    history_retention_period,
    history_retention_period_unit_desc
FROM sys.tables
WHERE temporal_type = 2  -- 2 = SYSTEM_VERSIONED_TEMPORAL
  AND name = 'Employees';

Disable và Re-Enable System Versioning

-- Disable system versioning (cần cho DDL changes)
ALTER TABLE dbo.Employees SET (SYSTEM_VERSIONING = OFF);
-- Sau khi disable: table vẫn tồn tại, history table vẫn tồn tại nhưng không còn tự động update

-- Thực hiện DDL changes trên current table
ALTER TABLE dbo.Employees 
ADD Email NVARCHAR(200) NULL;

-- Cũng cần update history table structure
ALTER TABLE dbo.Employees_History 
ADD Email NVARCHAR(200) NULL;

-- Re-enable system versioning
ALTER TABLE dbo.Employees
SET (SYSTEM_VERSIONING = ON (
    HISTORY_TABLE = dbo.Employees_History,
    DATA_CONSISTENCY_CHECK = ON
));

Temporal Table Limitations

LimitationMô tả
TRUNCATE TABLEKhông được phép — dùng DELETE thay thế
Period columnsKhông thể manually INSERT/UPDATE period columns qua normal DML
DELETE on historyKhông thể DELETE từ history table trực tiếp
Schema changesCần SYSTEM_VERSIONING = OFF trước khi ALTER TABLE
INSTEAD OF triggersKhông hỗ trợ trên temporal tables
Filestream columnsKhông hỗ trợ
In-Memory OLTPKhông kết hợp được với memory-optimized tables
Always EncryptedColumn-level encryption với AE có giới hạn
ReplicationMerge replication không hỗ trợ temporal tables
Table partitioningHistory table có thể partition (SQL 2017+)
-- Lỗi sẽ xảy ra:
TRUNCATE TABLE dbo.Employees;  
-- Error: Cannot truncate table 'dbo.Employees' because it is a system-versioned temporal table

-- Phải dùng DELETE thay thế
DELETE FROM dbo.Employees WHERE EmployeeID = 999;
-- Hoặc nếu muốn xóa nhiều: DELETE without WHERE (chậm hơn nhưng cho phép)

Temporal Table + DDL Management

-- Xem tất cả temporal tables trong database
SELECT 
    t.name AS current_table,
    ht.name AS history_table,
    t.temporal_type_desc,
    t.history_retention_period,
    t.history_retention_period_unit_desc
FROM sys.tables t
JOIN sys.tables ht ON t.history_table_id = ht.object_id
WHERE t.temporal_type = 2
ORDER BY t.name;

-- Tạo script để backup temporal configuration
SELECT 
    'ALTER TABLE ' + QUOTENAME(SCHEMA_NAME(t.schema_id)) + '.' + QUOTENAME(t.name) + 
    ' SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = ' + 
    QUOTENAME(SCHEMA_NAME(ht.schema_id)) + '.' + QUOTENAME(ht.name) + '));' AS enable_script
FROM sys.tables t
JOIN sys.tables ht ON t.history_table_id = ht.object_id
WHERE t.temporal_type = 2;

Temporal + Always On Availability Groups

-- Temporal tables hoạt động tốt với Always On AG:
-- - Current table và history table đều được replicate
-- - FOR SYSTEM_TIME queries có thể chạy trên readable secondary
-- - Minimal overhead vì không cần trigger

-- Trên secondary replica (readable):
-- Có thể query lịch sử nhưng KHÔNG thể write
SELECT * FROM dbo.Employees FOR SYSTEM_TIME AS OF '2023-01-01';
-- Works on secondary!

-- Best practice: Tạo nonclustered indexes trên history table cho historical query performance
CREATE INDEX IX_Employees_History_Department_Period
ON dbo.Employees_History (Department, ValidFrom, ValidTo)
INCLUDE (Name, Salary);

Q&A theo Cấp Độ

Junior Level

Q: Temporal Table là gì và tại sao nên dùng thay vì trigger + audit table thủ công?

A: Temporal Table là bảng có tính năng system-versioning tích hợp sẵn trong SQL Server 2016+. SQL Server tự động lưu lịch sử thay đổi vào history table khi có write operations. Dùng thay vì trigger vì:

  • Zero maintenance code (không cần viết trigger)
  • Atomic với transaction gốc (trigger có thể fail riêng lẻ)
  • Có syntax query đặc biệt FOR SYSTEM_TIME AS OF rất thuận tiện
  • Hiệu năng tốt hơn (không cần trigger execution overhead)

Q: Sự khác biệt giữa ValidFrom và ValidTo trong temporal table?

A: ValidFrom = thời điểm row này bắt đầu có hiệu lực (UTC, auto-set khi INSERT hoặc UPDATE). ValidTo = thời điểm row này hết hiệu lực. Row hiện tại có ValidTo = '9999-12-31...'. Row trong history table có ValidTo = thời điểm nó bị replace/delete.

Q: FOR SYSTEM_TIME AS OF và FOR SYSTEM_TIME ALL khác nhau thế nào?

A: AS OF 'datetime' chỉ trả về rows có hiệu lực tại thời điểm đó (như snapshot). ALL trả về mọi rows từ cả current table và history table — toàn bộ lịch sử.


Mid Level

Q: Tại sao không thể TRUNCATE một temporal table? Giải pháp thay thế?

A: TRUNCATE không cho phép vì SQL Server cần maintain tính nhất quán giữa current table và history table. TRUNCATE bỏ qua triggers và row-level tracking. Giải pháp:

  1. DELETE FROM dbo.TableName — xóa từng row (chậm nhưng an toàn, ghi history)
  2. Nếu muốn xóa cả history: Tắt system versioning, TRUNCATE cả hai tables, re-enable
  3. ALTER TABLE ... SET (SYSTEM_VERSIONING = OFF); TRUNCATE TABLE ...; TRUNCATE TABLE ..._History; ALTER TABLE ... SET (SYSTEM_VERSIONING = ON ...);

Q: Làm sao tối ưu performance của historical queries trên temporal table?

A:

  1. Index trên history table: SQL Server tự tạo clustered index trên (ValidTo, ValidFrom) hoặc (ValidFrom, ValidTo). Thêm nonclustered indexes cho các common query patterns (e.g., thêm EmployeeID, Department).
  2. Partitioning history table: Partition theo ValidFrom để aging out old data và pruning trong queries.
  3. Retention policy: Set HISTORY_RETENTION_PERIOD để tự cleanup data cũ, giảm history table size.
  4. Avoid FOR SYSTEM_TIME ALL khi không cần: Query chỉ history table trực tiếp nếu chỉ cần historical data.

Senior Level

Q: Temporal Table khác CDC thế nào về mặt kiến trúc và khi nào nên chọn cái nào?

A: Architecture:

  • Temporal: Synchronous, atomic với DML transaction. Write triggers immediate history insert trong same transaction.
  • CDC: Asynchronous, reads transaction log after the fact. Change capture jobs run separately.

Chọn Temporal khi: Cần point-in-time queries trong application, compliance audit, slowly changing dimensions, không muốn dependency SQL Agent, simplicity.

Chọn CDC khi: Cần feed downstream systems (data warehouse, event bus), cần capture before-values cho deletes trong consumer systems, cần fine-grained column-level change tracking, multiple downstream consumers consume at different rates.

Kết hợp cả hai: Temporal trên OLTP + CDC để extract changes sang data platform. Không conflict nhau.

Q: Giải thích cách thiết kế một audit system enterprise-grade dùng Temporal Tables cho một hệ thống có 100+ tables cần audit?

A: Approach:

  1. Selective auditing: Không phải tất cả 100 tables đều cần full temporal — phân loại tables theo sensitivity (PII, financial, config).

  2. Standardized period column naming: Convention nhất quán (AuditFrom/AuditTo hoặc ValidFrom/ValidTo) để automation có thể xử lý.

  3. Automated provisioning script:

-- Template để detect và enable temporal cho tables chưa có
SELECT 
    QUOTENAME(SCHEMA_NAME(schema_id)) + '.' + QUOTENAME(name)
FROM sys.tables
WHERE temporal_type = 0  -- Non-temporal
    AND name IN (SELECT TableName FROM dbo.AuditConfig WHERE IsEnabled = 1);
  1. Retention tiered: Bảng financial → 7 năm. Bảng session data → 90 ngày.

  2. History table partitioning cho tables lớn:

-- Partition history table theo year để efficient range queries và archival
CREATE PARTITION FUNCTION pf_AuditYear (DATETIME2)
AS RANGE RIGHT FOR VALUES ('2022-01-01', '2023-01-01', '2024-01-01', '2025-01-01');
  1. Centralized history reporting view:
-- UNION ALL across all temporal history tables với consistent schema
CREATE VIEW dbo.AuditTrail AS
SELECT 'Employees' AS TableName, CAST(EmployeeID AS NVARCHAR) AS EntityID, 
       ValidFrom, ValidTo FROM dbo.Employees_History
UNION ALL
SELECT 'Products', CAST(ProductID AS NVARCHAR), ValidFrom, ValidTo 
FROM dbo.Products_History;
  1. Schema change management: CI/CD pipeline phải automatically tắt system versioning, apply migration, update history table, re-enable. Dùng Flyway/Liquibase với custom callbacks.

In-Memory OLTP (Hekaton)

Overview

In-Memory OLTP (codename Hekaton) là tính năng được giới thiệu trong SQL Server 2014, cho phép lưu trữ tables trực tiếp trong RAM thay vì buffer pool truyền thống. Được thiết kế để xử lý workloads OLTP với throughput cực cao và latency thấp.

Tại sao In-Memory OLTP nhanh hơn?

Điểm khác biệtDisk-Based TablesMemory-Optimized Tables
StorageBuffer pool (disk-backed)RAM-only (+ optional durable write-ahead log)
LockingPessimistic lockingOptimistic multi-version concurrency control (MVCC)
LatchingBuffer pool latchesKhông có latches cho data access
LoggingFull transaction logReduced logging (chỉ insert/delete, không update-in-place)
Stored ProcsInterpreted T-SQLNatively compiled → machine code
IndexesB-tree on diskHash index hoặc Bw-tree (lock-free) trong RAM

Memory-Optimized Tables

Tạo Memory-Optimized Filegroup

Trước khi tạo memory-optimized tables, database phải có một memory-optimized filegroup:

-- Bước 1: Tạo database với memory-optimized filegroup
CREATE DATABASE InMemoryDemo
ON PRIMARY (NAME = 'InMemoryDemo_data', FILENAME = 'C:\Data\InMemoryDemo.mdf'),
FILEGROUP InMemory_FG CONTAINS MEMORY_OPTIMIZED_DATA
    (NAME = 'InMemory_FG_container', FILENAME = 'C:\Data\InMemory_FG_container')
LOG ON (NAME = 'InMemoryDemo_log', FILENAME = 'C:\Data\InMemoryDemo.ldf');

-- Bước 2 (nếu database đã tồn tại): Thêm filegroup vào database hiện có
ALTER DATABASE ExistingDB ADD FILEGROUP InMemory_FG CONTAINS MEMORY_OPTIMIZED_DATA;
ALTER DATABASE ExistingDB ADD FILE (
    NAME = 'InMemory_Container',
    FILENAME = 'C:\Data\InMemory_Container'
) TO FILEGROUP InMemory_FG;

Tạo Memory-Optimized Table

-- SCHEMA_AND_DATA: Data được persist xuống disk (durable)
CREATE TABLE dbo.ShoppingCart (
    CartID       INT NOT NULL,
    UserID       INT NOT NULL,
    ProductID    INT NOT NULL,
    Quantity     INT NOT NULL DEFAULT 1,
    AddedAt      DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(),
    CONSTRAINT PK_ShoppingCart PRIMARY KEY NONCLUSTERED (CartID),
    INDEX IX_ShoppingCart_UserID HASH (UserID) WITH (BUCKET_COUNT = 131072)
)
WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA);

-- SCHEMA_ONLY: Chỉ schema được persist, data mất sau restart (như temp table)
CREATE TABLE dbo.SessionCache (
    SessionKey   NVARCHAR(100) NOT NULL,
    SessionData  NVARCHAR(MAX) NULL,
    ExpiresAt    DATETIME2 NOT NULL,
    CONSTRAINT PK_SessionCache PRIMARY KEY NONCLUSTERED HASH (SessionKey)
        WITH (BUCKET_COUNT = 262144)
)
WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_ONLY);

DURABILITY Options

OptionMô tảUse Case
SCHEMA_AND_DATASchema + data đều durable, survive restartPermanent business data
SCHEMA_ONLYSchema durable, data mất sau restartSession state, work queues, temp data

Restrictions của Memory-Optimized Tables

-- KHÔNG hỗ trợ:
-- 1. FOREIGN KEY constraint (SQL 2014-2016, được hỗ trợ từ SQL 2017+)
-- 2. CHECK constraint chứa subquery
-- 3. IDENTITY column với TRUNCATE TABLE
-- 4. Computed columns (SQL 2014-2016)
-- 5. NULL columns trong PRIMARY KEY
-- 6. Varchar(MAX), nvarchar(MAX) trong primary key
-- 7. TRUNCATE TABLE
-- 8. DDL không thể chạy trong transaction với memory-optimized tables

-- SQL 2019+ đã relaxed nhiều restrictions:
-- - Parallel query plans
-- - SELECT DISTINCT, UNION, GROUP BY với non-aggregate columns
-- - JOIN với non-memory-optimized tables

In-Memory Indexes

Hash Index

Hash Index dành cho equality lookups (WHERE column = value). Không hỗ trợ range scans hay ORDER BY.

-- Tạo Hash Index
CREATE TABLE dbo.Products (
    ProductID    INT NOT NULL,
    SKU          NVARCHAR(50) NOT NULL,
    CategoryID   INT NOT NULL,
    Price        DECIMAL(10,2) NOT NULL,
    -- Hash index cho equality lookup theo ProductID
    CONSTRAINT PK_Products PRIMARY KEY NONCLUSTERED HASH (ProductID)
        WITH (BUCKET_COUNT = 1048576),
    -- Hash index cho lookup theo SKU
    INDEX IX_Products_SKU HASH (SKU) WITH (BUCKET_COUNT = 131072)
)
WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA);

BUCKET_COUNT — quan trọng nhất khi thiết kế Hash Index:

  • Quá nhỏ: Nhiều collisions, chậm hơn vì phải chain nhiều rows
  • Quá lớn: Lãng phí memory (mỗi bucket = 8 bytes)
  • Rule of thumb: 1-2x số distinct values dự kiến, làm tròn lên power of 2
-- Kiểm tra hash index utilization
SELECT 
    OBJECT_NAME(hs.object_id) AS table_name,
    i.name AS index_name,
    hs.total_bucket_count,
    hs.empty_bucket_count,
    hs.avg_chain_length,
    hs.max_chain_length,
    CAST(100.0 * hs.empty_bucket_count / hs.total_bucket_count AS DECIMAL(5,2)) AS empty_bucket_pct
FROM sys.dm_db_xtp_hash_index_stats hs
JOIN sys.indexes i ON hs.object_id = i.object_id AND hs.index_id = i.index_id;
-- avg_chain_length > 10 → BUCKET_COUNT quá nhỏ, cần tăng lên
-- empty_bucket_pct < 33% → BUCKET_COUNT quá nhỏ

Range Index (Nonclustered Memory-Optimized Index)

Range Index (Bw-tree) hỗ trợ range scans, ORDER BY, inequality predicates:

CREATE TABLE dbo.Orders (
    OrderID      INT NOT NULL,
    CustomerID   INT NOT NULL,
    OrderDate    DATETIME2 NOT NULL,
    TotalAmount  DECIMAL(15,2) NOT NULL,
    -- Range index: hỗ trợ WHERE OrderDate BETWEEN ... AND ...
    INDEX IX_Orders_OrderDate NONCLUSTERED (OrderDate DESC),
    -- Hash index: hỗ trợ WHERE CustomerID = @ID
    INDEX IX_Orders_CustomerID HASH (CustomerID) WITH (BUCKET_COUNT = 65536),
    CONSTRAINT PK_Orders PRIMARY KEY NONCLUSTERED (OrderID)
)
WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA);
Hash IndexRange (Nonclustered) Index
Equality (=)Rất nhanh O(1)OK
Range (<, >, BETWEEN)Không hỗ trợHỗ trợ
ORDER BYKhông hỗ trợHỗ trợ
Memory usageCố định (bucket_count * 8 bytes)Dynamic
Best forPrimary key lookups, high-frequency point queriesRange scans, sorted results

Natively Compiled Stored Procedures

Natively Compiled Stored Procedures được compile thành machine code (native DLL) khi tạo, thay vì interpret T-SQL mỗi lần chạy.

Tạo Native Procedure

CREATE OR ALTER PROCEDURE dbo.usp_AddToCart
    @CartID   INT,
    @UserID   INT,
    @ProductID INT,
    @Quantity INT
WITH NATIVE_COMPILATION, SCHEMABINDING
AS
BEGIN ATOMIC WITH (
    TRANSACTION ISOLATION LEVEL = SNAPSHOT,
    LANGUAGE = N'us_english'
)
    -- Chỉ có thể access memory-optimized tables
    IF EXISTS (SELECT 1 FROM dbo.ShoppingCart 
               WHERE CartID = @CartID AND ProductID = @ProductID)
    BEGIN
        UPDATE dbo.ShoppingCart
        SET Quantity = Quantity + @Quantity
        WHERE CartID = @CartID AND ProductID = @ProductID;
    END
    ELSE
    BEGIN
        INSERT INTO dbo.ShoppingCart (CartID, UserID, ProductID, Quantity)
        VALUES (@CartID, @UserID, @ProductID, @Quantity);
    END
END;

Restrictions của Native Procedures

-- KHÔNG hỗ trợ trong Native Procedures (SQL 2014-2016):
-- - TRY...CATCH
-- - Dynamic SQL (EXEC sp_executesql)
-- - Cursors
-- - SELECT INTO
-- - Subqueries trong nhiều contexts
-- - OUTER JOIN (SQL 2014)
-- - OR điều kiện (SQL 2014)
-- - Window functions (SQL 2014)
-- - CASE với subqueries
-- - String functions: LEN, SUBSTRING có giới hạn

-- SQL 2016+ đã mở rộng nhiều hơn
-- SQL 2019+ gần như full T-SQL surface area

BEGIN ATOMIC Block

Native procedures phải dùng BEGIN ATOMIC để đảm bảo atomicity:

-- BEGIN ATOMIC yêu cầu:
-- 1. TRANSACTION ISOLATION LEVEL: SNAPSHOT hoặc REPEATABLE READ hoặc SERIALIZABLE
-- 2. LANGUAGE
-- Không cần BEGIN TRAN/COMMIT TRAN → tự động atomic
BEGIN ATOMIC WITH (
    TRANSACTION ISOLATION LEVEL = SNAPSHOT,
    LANGUAGE = N'us_english',
    DELAYED_DURABILITY = ON  -- Optional: buffered logging cho performance
)
    -- T-SQL statements
END;

Performance Characteristics

-- Xem native procedure DLL location
SELECT 
    OBJECT_NAME(ps.object_id) AS proc_name,
    ps.cached_time,
    ps.last_execution_time,
    ps.execution_count,
    ps.total_worker_time / 1000 AS total_cpu_ms,
    ps.total_worker_time / NULLIF(ps.execution_count, 0) / 1000 AS avg_cpu_ms
FROM sys.dm_exec_procedure_stats ps
WHERE OBJECTPROPERTY(ps.object_id, 'IsNativelyCompiled') = 1
ORDER BY ps.total_worker_time DESC;

Memory-Optimized Table Types

Memory-Optimized Table Variables dùng như table variables nhưng được lưu trong RAM, tránh tempdb contention:

-- Tạo memory-optimized table type
CREATE TYPE dbo.OrderItemList AS TABLE (
    ProductID    INT NOT NULL,
    Quantity     INT NOT NULL,
    UnitPrice    DECIMAL(10,2) NOT NULL,
    INDEX IX_ProductID HASH (ProductID) WITH (BUCKET_COUNT = 1024)
)
WITH (MEMORY_OPTIMIZED = ON);

-- Dùng trong stored procedure
CREATE OR ALTER PROCEDURE dbo.ProcessBulkOrder
    @OrderItems dbo.OrderItemList READONLY  -- Phải là READONLY
AS
BEGIN
    -- Process items from in-memory table variable
    INSERT INTO dbo.OrderDetails (ProductID, Quantity, UnitPrice)
    SELECT ProductID, Quantity, UnitPrice
    FROM @OrderItems;
END;

-- Gọi procedure
DECLARE @Items dbo.OrderItemList;
INSERT INTO @Items VALUES (1, 5, 29.99), (2, 2, 49.99);
EXEC dbo.ProcessBulkOrder @OrderItems = @Items;

Migration Considerations

Xác định Candidates cho In-Memory OLTP

-- SQL Server cung cấp AMR (Analysis, Migrate, Report) tool
-- Hoặc dùng query để tìm hot tables
SELECT TOP 20
    OBJECT_NAME(ios.object_id) AS table_name,
    ios.row_lock_count + ios.page_lock_count AS total_locks,
    ios.row_lock_wait_count + ios.page_lock_wait_count AS total_lock_waits,
    ios.row_lock_wait_in_ms + ios.page_lock_wait_in_ms AS total_lock_wait_ms,
    ios.page_latch_wait_count,
    ios.page_latch_wait_in_ms
FROM sys.dm_db_index_operational_stats(DB_ID(), NULL, NULL, NULL) ios
JOIN sys.tables t ON ios.object_id = t.object_id
WHERE t.is_memory_optimized = 0  -- Chỉ disk-based tables
ORDER BY total_lock_waits DESC;

Surface Area Restrictions Check

-- Dùng stored procedure để check compatibility
-- (SQL Server Management Studio có built-in tool)
EXEC sys.sp_xtp_bind_db_resource_pool 
    @database_name = N'YourDatabase',
    @pool_name = N'InMemoryPool';

-- Kiểm tra tables có unsupported features không
SELECT 
    t.name AS table_name,
    c.name AS column_name,
    ty.name AS data_type
FROM sys.tables t
JOIN sys.columns c ON t.object_id = c.object_id
JOIN sys.types ty ON c.user_type_id = ty.user_type_id
WHERE ty.name IN ('text', 'ntext', 'image', 'xml', 'geography', 'geometry')
    -- Các data types này không hỗ trợ trong memory-optimized tables
ORDER BY t.name;

Performance Gains

Eliminating Lock/Latch Contention

In-Memory OLTP dùng Multi-Version Concurrency Control (MVCC):

  • Writers không block readers và ngược lại
  • No row locks, page locks, or table locks cho data access
  • Conflict detection dùng optimistic concurrency → write conflicts được detect lúc commit
  • Không có buffer pool latches → loại bỏ một nguồn contention lớn

Logging Optimization

-- Traditional tables: log full before/after page images
-- Memory-optimized tables: log chỉ logical operations (insert/delete pairs)
-- Không có update-in-place → mọi update = delete old version + insert new version

-- Delayed Durability: buffer log writes cho throughput cao hơn
-- (risk: data loss trong khoảng buffer window nếu crash)
ALTER DATABASE YourDB SET DELAYED_DURABILITY = FORCED;  -- Tất cả transactions

-- Hoặc per-procedure
BEGIN ATOMIC WITH (
    TRANSACTION ISOLATION LEVEL = SNAPSHOT,
    LANGUAGE = N'us_english',
    DELAYED_DURABILITY = ON
)

Monitoring In-Memory OLTP

sys.dm_db_xtp_* DMVs

-- Xem memory usage của memory-optimized objects
SELECT 
    OBJECT_NAME(object_id) AS table_name,
    memory_allocated_for_table_kb,
    memory_used_by_table_kb,
    memory_allocated_for_indexes_kb,
    memory_used_by_indexes_kb
FROM sys.dm_db_xtp_table_memory_stats
ORDER BY memory_used_by_table_kb DESC;

-- Xem XTP (In-Memory OLTP) runtime stats
SELECT * FROM sys.dm_xtp_system_memory_consumers;

-- Transactions conflict stats
SELECT 
    object_id,
    OBJECT_NAME(object_id) AS table_name,
    scans_started,
    read_committed_scans_started,
    write_conflicts,
    unique_constraint_violations
FROM sys.dm_db_xtp_object_stats
ORDER BY write_conflicts DESC;

-- Xem tất cả memory-optimized tables và metadata
SELECT 
    name,
    is_memory_optimized,
    durability,
    durability_desc
FROM sys.tables
WHERE is_memory_optimized = 1;

XTP-Specific Performance Counters

-- Dùng DMV để theo dõi
SELECT 
    counter_name,
    cntr_value
FROM sys.dm_os_performance_counters
WHERE object_name LIKE '%XTP%'
ORDER BY object_name, counter_name;

-- Các counters quan trọng:
-- XTP Transactions/sec
-- XTP Garbage Collections/sec  
-- XTP Checkpoint Files Merged/sec
-- XTP Log bytes written/sec

Buffer Pool Bypass

Data trong memory-optimized tables KHÔNG được lưu trong buffer pool:

  • Disk-based tables: data pages → buffer pool (RAM cache) ↔ disk
  • Memory-optimized tables: data chỉ tồn tại trong RAM, không qua buffer pool
  • Durability: SQL Server ghi checkpoint files (data/delta files) xuống disk để recover sau restart
  • Recovery: khi SQL Server restart, load data từ checkpoint files + transaction log vào RAM
-- Xem checkpoint file pairs (data + delta files)
SELECT 
    container_id,
    state_desc,
    file_type_desc,
    internal_storage_slot,
    checkpoint_pair_file_id,
    file_size_in_bytes / 1024 / 1024 AS size_mb,
    file_used_size_in_bytes / 1024 / 1024 AS used_mb
FROM sys.dm_db_xtp_checkpoint_files
ORDER BY container_id, file_type_desc;

When to Use In-Memory OLTP

Ideal Use Cases

Use CaseLý do phù hợp
High-frequency INSERT/UPDATELoại bỏ lock/latch contention
Session state storageSCHEMA_ONLY → nhanh, không cần persist
Work queues / messagingHigh-throughput enqueue/dequeue
Real-time risk/fraud scoringSub-millisecond lookup
Temporary data aggregationBuffer trước khi flush xuống disk-based tables
Hot lookup tablesReference data thường xuyên read

Khi KHÔNG nên dùng

  • Table có nhiều full-table scans (không phải OLTP pattern)
  • Data lớn hơn available RAM
  • Schema thay đổi thường xuyên (DDL hạn chế)
  • Cần complex SQL features chưa được hỗ trợ
  • Ad-hoc reporting queries (dùng columnstore index thay thế)

Q&A theo Cấp Độ

Junior Level

Q: In-Memory OLTP (Hekaton) là gì? Tại sao nó nhanh hơn?

A: In-Memory OLTP là tính năng từ SQL Server 2014, lưu toàn bộ data trong RAM thay vì disk. Nhanh hơn vì:

  1. Không cần I/O disk cho data access
  2. Loại bỏ locking bằng MVCC (writers không block readers)
  3. Không có buffer pool latches
  4. Native compiled procedures chạy thẳng machine code

Q: SCHEMA_AND_DATA vs SCHEMA_ONLY durability là gì?

A: SCHEMA_AND_DATA: data được ghi xuống disk (checkpoint + log), không mất sau restart — dùng cho permanent data. SCHEMA_ONLY: chỉ schema được lưu, data mất sau SQL Server restart — dùng cho session state, temp data không cần persist.

Q: Hash Index và Range Index trong In-Memory OLTP khác nhau thế nào?

A: Hash Index dùng hash function để lookup equality (WHERE col = value) trong O(1) time, không hỗ trợ range hay ORDER BY. Range Index (Bw-tree) hỗ trợ equality, range scans, ORDER BY nhưng chậm hơn Hash Index cho point lookups. BUCKET_COUNT của Hash Index phải được chọn cẩn thận.


Mid Level

Q: Làm sao chọn BUCKET_COUNT phù hợp cho Hash Index?

A: Rule of thumb:

  • Đặt bucket_count = 1-2x số distinct values của indexed column (không phải tổng số rows)
  • Làm tròn lên power of 2 gần nhất (4096, 8192, 16384, 131072, 262144…)
  • Monitor bằng sys.dm_db_xtp_hash_index_stats: nếu avg_chain_length > 10 → tăng bucket_count
  • Nếu empty_bucket_pct < 33% → tăng bucket_count
  • Nếu > 95% empty → giảm bucket_count để tiết kiệm memory

Q: Giải thích BEGIN ATOMIC trong Native Compiled Procedures?

A: BEGIN ATOMIC là bắt buộc trong native procedures, đảm bảo khối code chạy trong một unit (atomic). Khác với disk-based procedures:

  • Không cần BEGIN TRAN/COMMIT TRAN — tự động atomic
  • Phải chỉ định TRANSACTION ISOLATION LEVEL (SNAPSHOT thường dùng)
  • Phải chỉ định LANGUAGE
  • Có thể dùng DELAYED_DURABILITY = ON để buffer log writes

Senior Level

Q: Giải thích cơ chế MVCC trong In-Memory OLTP và tại sao nó tốt hơn locking truyền thống cho OLTP?

A: In-Memory OLTP dùng optimistic MVCC:

  • Mỗi row có begin_tsend_ts (transaction timestamps)
  • Readers luôn đọc phiên bản row tương ứng với thời điểm transaction bắt đầu → không chờ writers
  • Writers tạo new version của row (không modify in-place), set end_ts của version cũ = commit timestamp
  • Write-write conflict: nếu 2 transactions cùng update 1 row, transaction thứ hai bị abort
  • Garbage collection tự động dọn các row versions không còn cần
  • Tốt hơn locking: loại bỏ lock wait time, deadlock, lock escalation hoàn toàn

Q: Làm sao migrate một disk-based table sang In-Memory OLTP với zero downtime?

A: Quy trình migration an toàn:

  1. Analysis: Dùng AMR tool hoặc DMVs để check surface area restrictions
  2. Create memory-optimized equivalent: Tạo table mới với MEMORY_OPTIMIZED = ON
  3. Dual-write pattern: Application write vào cả 2 tables trong transition period
  4. Backfill: Copy data từ disk-based sang memory-optimized
  5. Switch reads: Chuyển reads sang memory-optimized table
  6. Stop dual-write: Sau khi verified, bỏ writes vào disk-based table
  7. Cleanup: Drop disk-based table

Thay thế cho stored procedures: tạo native procedure mới, rename hoặc dùng sp_rename để swap atomically.

Q: In-Memory OLTP ảnh hưởng thế nào đến Always On Availability Groups?

A:

  • Memory-optimized tables được replicate đầy đủ sang secondary replicas
  • Checkpoint files được stream sang secondary như disk-based data files
  • Transaction log entries cho memory-optimized operations được gửi sang secondary
  • Recovery trên secondary cũng load data từ checkpoint + log vào RAM
  • Caveats: secondary cần đủ RAM để chứa cả memory-optimized data
  • SCHEMA_ONLY tables: data không replicate (chỉ schema) → secondary sẽ có empty tables sau failover
  • Recommend: test failover thoroughly để đảm bảo memory requirements được đáp ứng trên secondary

High Availability & Disaster Recovery

Overview: Các yếu tố cần cân nhắc

RPO vs RTO Tradeoffs

Công nghệRPORTOReadable SecondaryTransparent FailoverChi phí
Always On AG (Sync)~0 (zero data loss)< 30 giâyCó (Enterprise)Có (với listener)Cao
Always On AG (Async)Vài giây< 30 giâyCó (Enterprise)ManualCao
FCI~0 (shared storage)1-5 phútKhôngCó (WSFC)Rất cao
Log ShippingVài phút - giờ30-60 phútCó (STANDBY)Không (manual)Thấp
Database Mirroring (deprecated)~0 (Sync)< 30 giâyKhôngCó (với witness)Trung bình
Replication (Transactional)Vài giâyTùyKhôngTrung bình
Azure Auto-failover GroupsVài giây< 1 phútCao

Synchronous vs Asynchronous Replication

Synchronous (Đồng bộ):
  Primary → Commit → Ghi log local → Gửi log đến Secondary
                                    ← Nhận ACK từ Secondary
  → Primary chỉ commit xong khi Secondary đã nhận và hardened log
  → RPO = 0 (zero data loss), nhưng latency tăng theo network

Asynchronous (Bất đồng bộ):
  Primary → Commit → Ghi log local → Return to client
                   → Gửi log đến Secondary (background)
  → Primary không chờ Secondary → latency thấp
  → RPO > 0 (có thể mất vài giây data khi failover)

Always On Availability Groups (AG)

Giải pháp HA cao cấp nhất của SQL Server (Enterprise Edition), cung cấp:

  • Database-level HA (không phải instance-level như FCI)
  • Readable secondaries
  • Automatic failover
  • Hỗ trợ Azure (Hybrid scenarios)

Kiến trúc

                    ┌─────────────────────────────────────┐
                    │     Windows Server Failover Cluster  │
                    │                                      │
  ┌──────────────┐  │  ┌──────────┐      ┌──────────┐    │
  │  Application │──┼─▶│ Listener │      │ WSFC     │    │
  └──────────────┘  │  │ (VNN)    │      │ Quorum   │    │
         │          │  └────┬─────┘      └──────────┘    │
         │          │       │                              │
         │          │  ┌────▼─────────────────────────┐  │
         │          │  │        AG Group               │  │
         │          │  │                               │  │
         │          │  │  ┌──────────┐  ┌──────────┐  │  │
         │          │  │  │ Primary  │  │Secondary │  │  │
         └──────────┼──┤  │ Replica  │◀▶│ Replica  │  │  │
    Read/Write      │  │  │ (Node 1) │  │ (Node 2) │  │  │
                    │  │  └──────────┘  └──────────┘  │  │
                    │  └──────────────────────────────┘  │
                    └─────────────────────────────────────┘

    ┌─────────────────┐      ┌───────────────────┐
    │ Read-Only       │─────▶│ Secondary Replica │
    │ Applications    │      │ (Node 2)          │
    └─────────────────┘      └───────────────────┘

Cấu hình AG cơ bản

-- Trên Primary: Enable AlwaysOn (phải restart SQL Server sau)
-- Thực hiện qua SSMS hoặc PowerShell:
-- Enable-SqlAlwaysOn -ServerInstance "SQL01" -Force

-- Bước 1: Tạo Master Key và Certificate (cho endpoint encryption)
USE master;
CREATE MASTER KEY ENCRYPTION BY PASSWORD = 'StrongP@ssword!';
CREATE CERTIFICATE AG_Cert
WITH SUBJECT = 'AG Endpoint Certificate',
START_DATE = '20260101',
EXPIRY_DATE = '20360101';

-- Bước 2: Tạo Endpoint
CREATE ENDPOINT HADR_endpoint
STATE = STARTED
AS TCP (LISTENER_PORT = 5022, LISTENER_IP = ALL)
FOR DATABASE_MIRRORING (
    AUTHENTICATION = CERTIFICATE AG_Cert,
    ENCRYPTION = REQUIRED ALGORITHM AES,
    ROLE = ALL
);

-- Bước 3: Backup cert để dùng trên secondary
BACKUP CERTIFICATE AG_Cert
TO FILE = '\\Shared\Certs\AG_Cert.cer';

-- Trên Secondary: tạo login, cert, endpoint tương tự

-- Bước 4: Tạo Availability Group (trên Primary)
CREATE AVAILABILITY GROUP [AG_Production]
WITH (
    AUTOMATED_BACKUP_PREFERENCE = SECONDARY,  -- Backup trên secondary
    DB_FAILOVER = ON,  -- Failover khi health check fail
    DTC_SUPPORT = NONE,
    CLUSTER_TYPE = WSFC
)
FOR DATABASE [ProductionDB], [SalesDB]
REPLICA ON
    N'SQL01'
    WITH (
        ENDPOINT_URL = N'TCP://SQL01.domain.com:5022',
        AVAILABILITY_MODE = SYNCHRONOUS_COMMIT,
        FAILOVER_MODE = AUTOMATIC,
        SEEDING_MODE = AUTOMATIC,
        SECONDARY_ROLE (ALLOW_CONNECTIONS = READ_ONLY,
                        READ_ONLY_ROUTING_URL = N'TCP://SQL01.domain.com:1433')
    ),
    N'SQL02'
    WITH (
        ENDPOINT_URL = N'TCP://SQL02.domain.com:5022',
        AVAILABILITY_MODE = SYNCHRONOUS_COMMIT,
        FAILOVER_MODE = AUTOMATIC,
        SEEDING_MODE = AUTOMATIC,
        SECONDARY_ROLE (ALLOW_CONNECTIONS = READ_ONLY,
                        READ_ONLY_ROUTING_URL = N'TCP://SQL02.domain.com:1433')
    );

-- Trên Secondary: join AG
ALTER AVAILABILITY GROUP [AG_Production] JOIN;
ALTER AVAILABILITY GROUP [AG_Production] GRANT CREATE ANY DATABASE;

Monitoring AG Status

-- Xem trạng thái AG
SELECT 
    ag.name AS availability_group,
    ar.replica_server_name,
    ar.availability_mode_desc,
    ar.failover_mode_desc,
    ars.role_desc,
    ars.synchronization_health_desc,
    ars.connected_state_desc
FROM sys.availability_groups ag
JOIN sys.availability_replicas ar ON ag.group_id = ar.group_id
JOIN sys.dm_hadr_availability_replica_states ars ON ar.replica_id = ars.replica_id;

-- Xem log send queue và redo queue
SELECT 
    db_name(drs.database_id) AS database_name,
    drs.log_send_queue_size AS log_send_queue_kb,
    drs.log_send_rate AS log_send_rate_kb_s,
    drs.redo_queue_size AS redo_queue_kb,
    drs.redo_rate AS redo_rate_kb_s,
    drs.synchronization_state_desc
FROM sys.dm_hadr_database_replica_states drs;

-- Manual failover (khi cần maintenance trên Primary)
ALTER AVAILABILITY GROUP [AG_Production] FAILOVER;

Availability Group Listener

-- Tạo listener (sau khi AG đã tạo)
ALTER AVAILABILITY GROUP [AG_Production]
ADD LISTENER N'AG_Listener' (
    WITH IP ((N'192.168.1.100', N'255.255.255.0')),
    PORT = 1433
);

-- Connection string dùng listener (ứng dụng kết nối đến đây, không quan tâm Primary là node nào)
-- "Server=AG_Listener,1433;Database=ProductionDB;Integrated Security=True;
--  ApplicationIntent=ReadWrite;MultiSubnetFailover=True"

-- Read-only routing (kết nối ReadOnly → tự động redirect đến secondary)
-- "Server=AG_Listener,1433;Database=ProductionDB;Integrated Security=True;
--  ApplicationIntent=ReadOnly"

-- Cấu hình read-only routing list
ALTER AVAILABILITY GROUP [AG_Production]
MODIFY REPLICA ON N'SQL01'
WITH (PRIMARY_ROLE (
    READ_ONLY_ROUTING_LIST = (N'SQL02', N'SQL01')  -- Ưu tiên SQL02 cho read
));

ALTER AVAILABILITY GROUP [AG_Production]
MODIFY REPLICA ON N'SQL02'
WITH (PRIMARY_ROLE (
    READ_ONLY_ROUTING_LIST = (N'SQL01', N'SQL02')
));

Always On Failover Cluster Instances (FCI)

Instance-level HA, khác với AG (database-level).

                    ┌─────────────────────────────────────┐
                    │   Windows Server Failover Cluster    │
                    │                                      │
  ┌──────────────┐  │  ┌──────────┐     ┌──────────┐     │
  │  Application │──┼─▶│  VNN     │     │  WSFC    │     │
  └──────────────┘  │  │ (Virtual │     │  Quorum  │     │
                    │  │ Network  │     └──────────┘     │
                    │  │  Name)   │                       │
                    │  └────┬─────┘                       │
                    │       │                              │
                    │  ┌────▼────────────────────────┐   │
                    │  │  SQL Server FCI Instance    │   │
                    │  │                             │   │
                    │  │  ┌──────────┐ ┌──────────┐ │   │
                    │  │  │  Node 1  │ │  Node 2  │ │   │
                    │  │  │ (Active) │ │(Passive) │ │   │
                    │  │  └────┬─────┘ └──────────┘ │   │
                    │  │       │                     │   │
                    │  │  ┌────▼──────────────────┐ │   │
                    │  │  │    Shared Storage      │ │   │
                    │  │  │ (SAN / Azure Shared   │ │   │
                    │  │  │  Disk / S2D)           │ │   │
                    │  │  └───────────────────────┘ │   │
                    │  └────────────────────────────┘   │
                    └─────────────────────────────────────┘

Đặc điểm FCI:

  • Tất cả nodes chia sẻ cùng một storage
  • Chỉ một node Active tại một thời điểm
  • Failover: SQL Server instance chuyển sang node khác (shared storage vẫn accessible)
  • Không có readable secondary
  • Protect toàn bộ SQL Server instance (tất cả databases, SQL Agent jobs, logins)
  • License: chỉ cần license cho active nodes

Log Shipping

Giải pháp HA đơn giản, chi phí thấp — automated backup → copy → restore.

  ┌─────────────┐    Backup Log     ┌─────────────┐
  │   Primary   │─────────────────▶ │   Monitor   │
  │   (Source)  │                   │   Server    │
  └──────┬──────┘                   └──────┬──────┘
         │                                  │
         │ Backup files (shared folder)     │ Alert if delay
         │                                  │
         ▼                                  ▼
  ┌──────────────┐    Copy + Restore  ┌──────────────┐
  │  .trn files  │──────────────────▶ │  Secondary   │
  │  (Share)     │                   │  (DR Server) │
  └──────────────┘                   └──────────────┘
-- Cấu hình Log Shipping qua SSMS hoặc T-SQL
-- Primary: setup backup job
-- Primary database phải ở FULL recovery model

EXEC master.dbo.sp_add_log_shipping_primary_database
    @database = N'ProductionDB',
    @backup_directory = N'\\BackupShare\LogShipping',
    @backup_share = N'\\BackupShare\LogShipping',
    @backup_job_name = N'LSBackup_ProductionDB',
    @backup_retention_period = 4320,  -- 3 days (minutes)
    @backup_threshold = 60,  -- Alert nếu backup delay > 60 phút
    @threshold_alert_enabled = 1,
    @history_retention_period = 5760;  -- Keep history 4 days

-- Secondary: setup copy + restore jobs
EXEC master.dbo.sp_add_log_shipping_secondary_database
    @secondary_database = N'ProductionDB',
    @primary_server = N'SQL01',
    @primary_database = N'ProductionDB',
    @restore_delay = 0,
    @restore_mode = 0,          -- 0 = NORECOVERY, 1 = STANDBY (readable)
    @disconnect_users = 0,
    @restore_threshold = 45,    -- Alert nếu restore delay > 45 phút
    @threshold_alert_enabled = 1,
    @history_retention_period = 5760;

-- Xem trạng thái Log Shipping
SELECT 
    primary_database,
    backup_threshold,
    time_since_last_backup,
    last_backup_file
FROM msdb.dbo.log_shipping_monitor_primary;

SELECT 
    secondary_database,
    restore_threshold,
    time_since_last_restore,
    last_restored_file
FROM msdb.dbo.log_shipping_monitor_secondary;

Standby Mode: Secondary Read-Only

-- Restore với STANDBY: secondary database read-only (nhưng disconnect khi restore job chạy)
RESTORE LOG ProductionDB
FROM DISK = 'D:\LogShipping\ProductionDB_Log.trn'
WITH STANDBY = 'D:\LogShipping\Standby_Undo.bak';
-- Database trở thành read-only, users có thể query
-- Khi job restore log kế tiếp: disconnect users, apply log, back to read-only

Database Mirroring (Deprecated nhưng vẫn được hỏi)

Lưu ý: Deprecated từ SQL Server 2012, removed trong Azure SQL. Dùng AG thay thế.

  ┌──────────────┐  sync/async  ┌──────────────┐
  │  Principal   │◀────────────▶│    Mirror    │
  │  (Primary)   │              │  (Secondary) │
  └──────┬───────┘              └──────────────┘
         │                              │
         └──────────────────────────────┤
                        │               │
                   ┌────▼────┐          │
                   │ Witness │◀─────────┘
                   │(optional│
                   │for auto │
                   │failover)│
                   └─────────┘
ModeÝ nghĩa
High Safety (Synchronous)+ Witness → Automatic failover; không có witness → Manual
High Performance (Asynchronous)Không có witness, manual failover, có thể mất data

Replication

Sao chép dữ liệu giữa databases — không phải HA mà là data distribution.

  ┌─────────────┐  ┌──────────────┐  ┌─────────────┐
  │  Publisher  │─▶│ Distributor  │─▶│ Subscriber  │
  │   (Source)  │  │  (Metadata + │  │ (Destination│
  │             │  │   queue)     │  │    copy)    │
  └─────────────┘  └──────────────┘  └─────────────┘

So sánh các loại Replication

LoạiHoạt độngRPOUse Case
SnapshotCopy toàn bộ data định kỳHoursLookup tables, ít thay đổi
TransactionalStream từng transactionGiâyReal-time reporting, OLAP offload
MergeSync 2 chiều, conflict resolutionPhútMobile, distributed updates
Peer-to-PeerMulti-master transactionalGiâyGeo-distributed OLTP (phức tạp)
-- Xem replication status
SELECT * FROM distribution.dbo.MSdistribution_status;

-- Check replication latency (để monitor)
-- Thêm tracer token
EXEC sp_posttracertoken
    @publication = N'MyPublication',
    @tracer_token_syntax = N'This is a tracer token %s',
    @publisher_db = N'PublisherDB';

Azure SQL High Availability

Built-in HA (PaaS)

Azure SQL Database / Managed Instance có HA built-in — không cần cấu hình:

  • General Purpose / Standard: storage-level redundancy, failover ~30 giây
  • Business Critical / Premium: AG-like, local SSD, readable secondaries, failover < 30 giây
  • Hyperscale: scale-out reads với nhiều read replicas

Active Geo-Replication

-- Tạo readable secondary ở region khác (Azure SQL Database)
-- Thực hiện qua Azure Portal hoặc PowerShell:
-- New-AzSqlDatabaseSecondary -ResourceGroupName "rg1" 
--     -ServerName "primaryserver" -DatabaseName "mydb"
--     -PartnerResourceGroupName "rg2" -PartnerServerName "secondaryserver"

-- Failover thủ công
-- Set-AzSqlDatabaseSecondary -ResourceGroupName "rg2"
--     -ServerName "secondaryserver" -DatabaseName "mydb" -Failover

-- Hoàn toàn qua T-SQL với ALTER DATABASE (trong Azure SQL)
ALTER DATABASE mydb FAILOVER;

Auto-failover Groups

-- Auto-failover group: wrapper trên geo-replication + listener endpoint
-- Connection string: không cần biết primary/secondary
-- "Server=group-name.database.windows.net;Database=mydb;..."
-- Read-only: "Server=group-name.secondary.database.windows.net;..."

Database Snapshots như một cơ chế bảo vệ tạm thời

-- Tạo snapshot trước khi maintenance lớn
CREATE DATABASE ProductionDB_PreMaintenance
ON (NAME = 'ProductionDB_Data',
    FILENAME = 'D:\Snapshots\ProdDB_PreMaintenance.ss')
AS SNAPSHOT OF ProductionDB;

-- Sau maintenance nếu có vấn đề → revert
RESTORE DATABASE ProductionDB
FROM DATABASE_SNAPSHOT = 'ProductionDB_PreMaintenance';

-- Snapshot KHÔNG phải backup thực sự:
-- - Phụ thuộc source database
-- - Không protect khỏi server failure
-- - Không thể backup/restore riêng lẻ

Q&A - Phỏng vấn High Availability

Junior Level

Q1: Always On AG và FCI khác nhau thế nào?

Always On AGFCI
ScopeDatabase-levelInstance-level
StorageMỗi replica có storage riêngShared storage
Readable secondaryCó (Enterprise)Không
LicensePer-core tất cả replicasChỉ active nodes
Failover granularityTừng database/AGToàn bộ instance

Q2: Log Shipping là gì? Khi nào nên dùng?

Log Shipping tự động backup transaction log từ Primary, copy sang Secondary server, và restore. Secondary ở NORECOVERY hoặc STANDBY (read-only).

Dùng khi: ngân sách hạn chế, RPO 15-60 phút là chấp nhận được, cần readable DR server. Không dùng khi cần RPO gần zero hoặc automatic failover.

Q3: Sự khác biệt giữa synchronous và asynchronous AG?

  • Synchronous: Primary chờ Secondary hardened log trước khi commit → RPO = 0, nhưng độ trễ tăng (chỉ dùng trong cùng datacenter hoặc low-latency WAN)
  • Asynchronous: Primary không chờ Secondary → RPO > 0 (có thể mất vài giây data), nhưng không ảnh hưởng latency → dùng cho geo-distributed DR replicas

Mid Level

Q4: Readable Secondary trong AG hoạt động thế nào? Có vấn đề gì?

Secondary replica apply log từ Primary. Khi nhận log nhưng chưa apply, queries trên secondary đọc data cũ → potential dirty reads? Không — secondary dùng row versioning (snapshot isolation) để queries đọc consistent snapshot, không block redo operations.

Vấn đề:

  • Snapshot isolation sử dụng tempdb trên secondary → monitor tempdb usage
  • Queries nặng trên secondary có thể ảnh hưởng redo process (thường không đáng kể)
  • Data có thể lag vài milliseconds so với Primary

Q5: AG Listener làm gì? Transparent failover hoạt động thế nào?

Listener là Virtual Network Name (VNN) hoặc Distributed Network Name (DNN) — application kết nối đến Listener, không kết nối trực tiếp đến node. Khi failover:

  1. Secondary become Primary
  2. Listener routing update (< vài giây với DNN)
  3. Application reconnect đến Listener → tự động kết nối đến Primary mới

Application phải handle reconnection (retry logic). Connection string cần MultiSubnetFailover=True để reconnect nhanh trong multi-subnet AG.

Q6: Khi nào chọn Always On AG vs Azure Active Geo-Replication?

  • On-premises / SQL Server trong VM: Always On AG
  • Azure SQL Database: Active Geo-Replication hoặc Auto-failover Groups (PaaS, managed)
  • Azure SQL Managed Instance: Auto-failover groups (AG built-in, managed)
  • Hybrid (on-prem + Azure): AG với Azure replica (Disaster Recovery to Azure)

Senior Level

Q7: Thiết kế HA/DR solution cho ứng dụng banking: RPO = 0, RTO < 1 phút, có DR site?

Solution:

  • Primary DC: Always On AG với 2 nodes, synchronous commit, automatic failover
    • Node 1: Primary
    • Node 2: Synchronous secondary (same DC, auto failover)
  • DR DC: Node 3, asynchronous commit (cross-DC, RPO = vài giây)
  • Windows Server Failover Cluster: spanning cả 2 DCs
  • Listener: DNN (vì DNN nhanh hơn VNN khi failover)

RPO:

  • Trong cùng DC (Node 1 → 2): RPO = 0 (synchronous)
  • DR failover (Node 1/2 → 3): RPO = vài giây (asynchronous)

RTO:

  • Trong DC: < 30 giây (automatic failover)
  • DR failover: < 1 phút (manual hoặc WSFC cross-DC failover)

Q8: AG split-brain scenario là gì? WSFC quorum giải quyết thế nào?

Split-brain: Hai nodes mất kết nối với nhau nhưng cả hai đều nghĩ mình là Primary → cả hai process writes → data divergence.

WSFC Quorum ngăn chặn split-brain: một node/cluster chỉ active khi đạt quorum (majority):

  • Node Majority: > 50% nodes votes
  • Node and File Share Majority: nodes + file share witness vote
  • Node and Disk Majority: nodes + disk witness vote
  • Cloud Witness (SQL Server 2016+): Azure Blob Storage làm witness

Nếu một DC bị cô lập và không đạt quorum → nodes đó tự shut down SQL Server → không thể process writes → no split-brain.

Q9: Làm thế nào để achieve zero-downtime maintenance trên AG Primary?

Planned maintenance (patch, hardware):

  1. Force failover to secondary (pre-check secondary health)
  2. Thực hiện maintenance trên node cũ (giờ là secondary)
  3. Về sau failover lại về original node (nếu cần)
  4. Với read-only routing: ứng dụng đọc không bị ảnh hưởng

Online maintenance options:

  • Index rebuild với ONLINE = ON: không block reads/writes
  • Index maintenance per-partition: không cần lock toàn bảng
  • Statistics update: ASYNC_STATS_UPDATE (SQL Server 2017+)
  • Schema changes: cần test carefully, có thể cần maintenance window

Change Data Capture & Change Tracking

Change Data Capture (CDC)

CDC là gì?

Change Data Capture (CDC) là tính năng theo dõi và ghi lại INSERT, UPDATE, DELETE trên các tables được chỉ định. CDC đọc từ transaction log và lưu trữ cả giá trị trước (before)sau (after) khi data thay đổi vào các change tables riêng.

Đặc điểm:

  • Không ảnh hưởng đến hiệu năng của source table (asynchronous, đọc từ log)
  • Yêu cầu SQL Server Agent để chạy capture và cleanup jobs
  • Phù hợp cho ETL/data integration workloads
  • Lưu trữ full before/after values

Bật CDC

Bật CDC ở cấp Database

-- Bước 1: Bật CDC cho database
USE YourDatabase;
EXEC sys.sp_cdc_enable_db;

-- Kiểm tra CDC đã bật chưa
SELECT name, is_cdc_enabled 
FROM sys.databases 
WHERE name = 'YourDatabase';

Bật CDC ở cấp Table

-- Bước 2: Bật CDC cho từng table
USE YourDatabase;
EXEC sys.sp_cdc_enable_table
    @source_schema      = N'dbo',
    @source_name        = N'Orders',
    @role_name          = NULL,          -- NULL = không giới hạn access
    @capture_instance   = N'dbo_Orders', -- Tên instance (tối đa 100 chars)
    @supports_net_changes = 1,           -- Cho phép fn_cdc_get_net_changes
    @captured_column_list = NULL;        -- NULL = capture tất cả columns
    -- @captured_column_list = N'OrderID, CustomerID, Amount' -- Chỉ capture một số columns

-- Tắt CDC cho một table
EXEC sys.sp_cdc_disable_table
    @source_schema    = N'dbo',
    @source_name      = N'Orders',
    @capture_instance = N'dbo_Orders';

-- Kiểm tra các tables đang được CDC theo dõi
SELECT 
    ct.capture_instance,
    OBJECT_NAME(ct.source_object_id) AS source_table,
    ct.supports_net_changes,
    ct.has_drop_pending,
    ct.index_name,
    ct.captured_column_list
FROM cdc.change_tables ct;

CDC Change Table Structure

Khi CDC bật cho dbo.Orders, SQL Server tạo change table: cdc.dbo_Orders_CT

-- Xem cấu trúc của change table
SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, IS_NULLABLE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = 'cdc' AND TABLE_NAME = 'dbo_Orders_CT'
ORDER BY ORDINAL_POSITION;

-- Các system columns được thêm vào change table:
-- __$start_lsn       : LSN của transaction gây ra change (kiểu binary(10))
-- __$end_lsn         : Luôn NULL (reserved)
-- __$seqval          : Sequence value trong transaction
-- __$operation       : 1=DELETE, 2=INSERT, 3=UPDATE before, 4=UPDATE after
-- __$update_mask     : Bitmask chỉ ra columns nào bị thay đổi
-- [source_columns]   : Các columns từ source table

LSN (Log Sequence Number)

LSN là giá trị duy nhất xác định vị trí trong transaction log. CDC dùng LSN để theo dõi khoảng thời gian cần query changes.

-- Lấy LSN min và max hiện tại cho một capture instance
DECLARE @from_lsn binary(10) = sys.fn_cdc_get_min_lsn(N'dbo_Orders');
DECLARE @to_lsn   binary(10) = sys.fn_cdc_get_max_lsn();

SELECT 
    sys.fn_cdc_get_min_lsn(N'dbo_Orders') AS min_lsn,
    sys.fn_cdc_get_max_lsn()               AS current_max_lsn;

-- Convert LSN sang datetime và ngược lại
SELECT sys.fn_cdc_map_lsn_to_time(@from_lsn) AS from_time;
SELECT sys.fn_cdc_map_time_to_lsn('smallest greater than or equal', 
    DATEADD(DAY, -1, GETDATE())) AS yesterday_lsn;

Consume CDC Changes

fn_cdc_get_all_changes — Lấy mọi thay đổi (kể cả trung gian)

DECLARE @from_lsn binary(10) = sys.fn_cdc_get_min_lsn(N'dbo_Orders');
DECLARE @to_lsn   binary(10) = sys.fn_cdc_get_max_lsn();

-- Lấy tất cả changes (bao gồm UPDATE before & after)
SELECT 
    __$start_lsn,
    __$operation,
    CASE __$operation
        WHEN 1 THEN 'DELETE'
        WHEN 2 THEN 'INSERT'
        WHEN 3 THEN 'UPDATE (before)'
        WHEN 4 THEN 'UPDATE (after)'
    END AS operation_desc,
    sys.fn_cdc_map_lsn_to_time(__$start_lsn) AS change_time,
    OrderID,
    CustomerID,
    Amount,
    Status
FROM cdc.fn_cdc_get_all_changes_dbo_Orders(
    @from_lsn, @to_lsn, N'all'  -- 'all' hoặc 'all update old'
)
ORDER BY __$start_lsn, __$seqval;

fn_cdc_get_net_changes — Chỉ lấy trạng thái cuối cùng

-- Net changes: nếu 1 row INSERT rồi UPDATE 3 lần → chỉ thấy 1 INSERT với giá trị cuối
-- Cần @supports_net_changes = 1 khi enable CDC
SELECT 
    __$start_lsn,
    __$operation,
    OrderID,
    CustomerID,
    Amount
FROM cdc.fn_cdc_get_net_changes_dbo_Orders(
    @from_lsn, @to_lsn, N'all'
)
ORDER BY __$start_lsn;

Incremental Load Pattern (ETL)

-- Lưu LSN đã xử lý vào control table
CREATE TABLE dbo.CDC_Checkpoint (
    CaptureInstance NVARCHAR(100) PRIMARY KEY,
    LastProcessedLSN BINARY(10) NOT NULL,
    LastRunTime DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME()
);

-- ETL logic
DECLARE @from_lsn binary(10), @to_lsn binary(10);

-- Lấy LSN từ lần chạy trước
SELECT @from_lsn = ISNULL(
    (SELECT sys.fn_cdc_increment_lsn(LastProcessedLSN) 
     FROM dbo.CDC_Checkpoint WHERE CaptureInstance = 'dbo_Orders'),
    sys.fn_cdc_get_min_lsn('dbo_Orders')  -- Lần đầu chạy
);

SET @to_lsn = sys.fn_cdc_get_max_lsn();

-- Xử lý changes
INSERT INTO dbo.Orders_Staging (OrderID, CustomerID, Amount, ChangeType, ChangeTime)
SELECT 
    OrderID,
    CustomerID,
    Amount,
    CASE __$operation WHEN 1 THEN 'D' WHEN 2 THEN 'I' WHEN 4 THEN 'U' END,
    sys.fn_cdc_map_lsn_to_time(__$start_lsn)
FROM cdc.fn_cdc_get_net_changes_dbo_Orders(@from_lsn, @to_lsn, N'all')
WHERE __$operation IN (1, 2, 4);  -- DELETE, INSERT, UPDATE after

-- Cập nhật checkpoint
MERGE dbo.CDC_Checkpoint AS target
USING (SELECT 'dbo_Orders' AS CaptureInstance, @to_lsn AS LSN) AS src
ON target.CaptureInstance = src.CaptureInstance
WHEN MATCHED THEN UPDATE SET LastProcessedLSN = src.LSN, LastRunTime = SYSUTCDATETIME()
WHEN NOT MATCHED THEN INSERT (CaptureInstance, LastProcessedLSN) VALUES (src.CaptureInstance, src.LSN);

CDC Cleanup và Capture Jobs

CDC tự động tạo 2 SQL Agent Jobs:

  1. cdc.YourDatabase_capture — đọc log và ghi vào change tables
  2. cdc.YourDatabase_cleanup — xóa change records cũ hơn retention period
-- Xem CDC jobs
SELECT name, enabled, description
FROM msdb.dbo.sysjobs
WHERE name LIKE 'cdc.%'
ORDER BY name;

-- Cấu hình retention period (giây) — default 4320 phút = 3 ngày
EXEC sys.sp_cdc_change_job
    @job_type     = N'cleanup',
    @retention    = 10080,  -- 7 ngày = 7 * 24 * 60
    @threshold    = 5000;   -- Max rows deleted per cleanup cycle

-- Xem cấu hình hiện tại của CDC jobs
EXEC sys.sp_cdc_help_jobs;

-- Manual cleanup nếu cần
EXEC sys.sp_cdc_cleanup_change_table
    @capture_instance = N'dbo_Orders',
    @low_water_mark   = NULL,  -- NULL = dùng retention period
    @threshold        = 5000;

CDC Limitations

  • Yêu cầu SQL Server Agent (không hoạt động trên Express Edition)
  • Data types không hỗ trợ: không cho phép capture LOB columns nếu row vượt 8060 bytes? (thực ra LOB được hỗ trợ từ SQL 2012+)
  • Columns có kiểu timestamp/rowversion không được capture
  • Không hoạt động với In-Memory OLTP tables
  • DDL changes: nếu thay đổi schema table, cần disable rồi re-enable CDC
  • Không hỗ trợ database tham gia replication trong một số kịch bản
  • Performance overhead: scan log liên tục, ghi vào change tables

Change Tracking

Change Tracking là gì?

Change Tracking (CT) là giải pháp nhẹ hơn CDC: chỉ tracking rows nào đã thay đổi (INSERT/UPDATE/DELETE) nhưng KHÔNG lưu before/after values. Phù hợp cho data synchronization scenarios.

Đặc điểm:

  • Không cần SQL Agent
  • Overhead thấp hơn CDC
  • Automatic cleanup dựa trên retention period
  • Cần query source table để lấy current values

Bật Change Tracking

-- Bước 1: Bật Change Tracking ở cấp Database
ALTER DATABASE YourDatabase
SET CHANGE_TRACKING = ON
(CHANGE_RETENTION = 7 DAYS,  -- Giữ history 7 ngày
 AUTO_CLEANUP = ON);          -- Tự động dọn history cũ

-- Bước 2: Bật Change Tracking cho từng table
ALTER TABLE dbo.Products
ENABLE CHANGE_TRACKING
WITH (TRACK_COLUMNS_UPDATED = ON);  -- Track columns nào bị update

-- Tắt Change Tracking cho table
ALTER TABLE dbo.Products DISABLE CHANGE_TRACKING;

-- Tắt Change Tracking cho database (phải tắt tất cả tables trước)
ALTER DATABASE YourDatabase SET CHANGE_TRACKING = OFF;

-- Kiểm tra CT status
SELECT 
    name,
    is_change_tracking_on,
    is_track_columns_updated_on
FROM sys.change_tracking_tables ct
JOIN sys.tables t ON ct.object_id = t.object_id;

Query Changes với CHANGETABLE

-- Lấy current version
DECLARE @current_version BIGINT = CHANGE_TRACKING_CURRENT_VERSION();
DECLARE @sync_version BIGINT = 0;  -- Lấy từ lần sync trước

-- Lấy tất cả changes kể từ @sync_version
SELECT 
    ct.ProductID,
    ct.SYS_CHANGE_OPERATION,  -- 'I' = Insert, 'U' = Update, 'D' = Delete
    ct.SYS_CHANGE_VERSION,
    ct.SYS_CHANGE_CREATION_VERSION,
    -- Join với source table để lấy current values (chỉ cho I và U)
    p.ProductName,
    p.Price,
    p.CategoryID
FROM CHANGETABLE(CHANGES dbo.Products, @sync_version) AS ct
LEFT JOIN dbo.Products p ON ct.ProductID = p.ProductID
ORDER BY ct.SYS_CHANGE_VERSION;

-- Lấy version hiện tại của một row cụ thể
SELECT * FROM CHANGETABLE(VERSION dbo.Products, (ProductID), (42)) AS ct;

Sync Pattern với Change Tracking

-- Synchronization pattern
DECLARE @last_sync_version BIGINT;

-- 1. Lấy last sync version từ client/control table
SELECT @last_sync_version = LastVersion 
FROM dbo.CT_SyncCheckpoint 
WHERE ClientID = 'ClientA';

-- 2. Kiểm tra version đủ cũ chưa (không bị cleanup)
IF @last_sync_version < CHANGE_TRACKING_MIN_VALID_VERSION(OBJECT_ID('dbo.Products'))
BEGIN
    -- Phải full sync lại vì history đã bị cleanup
    RAISERROR('Sync version too old, full sync required', 16, 1);
    RETURN;
END

-- 3. Lấy changes và current values
SELECT 
    ct.ProductID,
    ct.SYS_CHANGE_OPERATION,
    p.ProductName,
    p.Price
FROM CHANGETABLE(CHANGES dbo.Products, @last_sync_version) ct
LEFT JOIN dbo.Products p ON ct.ProductID = p.ProductID;

-- 4. Cập nhật sync version
UPDATE dbo.CT_SyncCheckpoint 
SET LastVersion = CHANGE_TRACKING_CURRENT_VERSION(), 
    LastSync = SYSUTCDATETIME()
WHERE ClientID = 'ClientA';

TRACK_COLUMNS_UPDATED

-- Nếu TRACK_COLUMNS_UPDATED = ON, có thể biết column nào bị update
SELECT 
    ct.ProductID,
    ct.SYS_CHANGE_OPERATION,
    CHANGE_TRACKING_IS_COLUMN_IN_MASK(
        COLUMNPROPERTY(OBJECT_ID('dbo.Products'), 'Price', 'ColumnId'),
        ct.SYS_CHANGE_COLUMNS
    ) AS price_changed,
    CHANGE_TRACKING_IS_COLUMN_IN_MASK(
        COLUMNPROPERTY(OBJECT_ID('dbo.Products'), 'ProductName', 'ColumnId'),
        ct.SYS_CHANGE_COLUMNS
    ) AS name_changed
FROM CHANGETABLE(CHANGES dbo.Products, @sync_version) ct
WHERE ct.SYS_CHANGE_OPERATION = 'U';

So sánh CDC vs Change Tracking

Tiêu chíChange Data CaptureChange Tracking
Before valuesCó (lưu before/after)Không
After valuesPhải query source table
Deleted row dataCó (before values)Chỉ biết row bị xóa (PK only)
SQL Agent requiredKhông
Storage overheadCao (full row versions)Thấp (chỉ PK + metadata)
LatencyCó độ trễ (async log read)Gần như realtime (sync)
CleanupDựa trên retention (có SQL Agent job)Auto cleanup (tự động)
Use case chínhETL, data integration, auditData synchronization giữa systems
Memory-Optimized tablesKhông hỗ trợKhông hỗ trợ
Point-in-time historyĐầy đủ (mọi change)Chỉ net change từ version
ComplexityCao hơnThấp hơn
Giá thành về performanceCao hơn (ghi change table)Thấp hơn

Temporal Tables (So sánh với CDC)

System-Versioned Temporal Tables

Temporal Tables (SQL Server 2016+, SQL Standard 2011) tự động lưu toàn bộ lịch sử thay đổi trong một history table riêng.

-- Tạo Temporal Table từ đầu
CREATE TABLE dbo.Employees (
    EmployeeID   INT NOT NULL PRIMARY KEY,
    Name         NVARCHAR(100) NOT NULL,
    Department   NVARCHAR(50),
    Salary       DECIMAL(15,2),
    -- Period columns (auto-managed by SQL Server)
    ValidFrom    DATETIME2 GENERATED ALWAYS AS ROW START NOT NULL,
    ValidTo      DATETIME2 GENERATED ALWAYS AS ROW END NOT NULL,
    PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo)
)
WITH (
    SYSTEM_VERSIONING = ON (
        HISTORY_TABLE = dbo.Employees_History,
        DATA_CONSISTENCY_CHECK = ON
    )
);

Query Temporal History

-- Xem data tại một thời điểm cụ thể
SELECT * FROM dbo.Employees
FOR SYSTEM_TIME AS OF '2024-01-01 12:00:00';

-- Xem tất cả changes trong khoảng thời gian
SELECT * FROM dbo.Employees
FOR SYSTEM_TIME FROM '2024-01-01' TO '2024-12-31';

CDC vs Temporal Tables

Tiêu chíCDCTemporal Tables
Mục đích chínhData integration/ETLAudit trail, point-in-time query
Before/AfterExplicit (operation column)Tự động (history table)
Query syntaxfn_cdc_get_all_changesFOR SYSTEM_TIME AS OF
LatencyCó độ trễ (async)Synchronous (ngay lập tức)
SQL AgentCầnKhông cần
CleanupConfigurable retentionConfigurable HISTORY_RETENTION_PERIOD
DDL compatibilityRequires re-enable on schema changeAutomatically handled
GranularityColumn-level maskRow-level chỉ (ValidFrom/ValidTo)
Phù hợp nhấtETL pipelines, downstream systemsSlowly changing dimensions, compliance audit

Q&A theo Cấp Độ

Junior Level

Q: CDC và Change Tracking khác nhau thế nào?

A: CDC lưu đầy đủ before/after values của mọi change và yêu cầu SQL Agent — phù hợp ETL. Change Tracking chỉ ghi nhận rows nào thay đổi (không có before/after values), không cần SQL Agent, overhead nhỏ hơn — phù hợp data sync giữa systems.

Q: Làm sao enable CDC cho một table?

A:

-- Bước 1: Enable ở database level
EXEC sys.sp_cdc_enable_db;

-- Bước 2: Enable cho table
EXEC sys.sp_cdc_enable_table
    @source_schema = N'dbo',
    @source_name = N'YourTable',
    @role_name = NULL,
    @supports_net_changes = 1;

Q: LSN là gì trong CDC context?

A: LSN (Log Sequence Number) là số thứ tự duy nhất trong transaction log, xác định vị trí chính xác của mỗi log record. CDC dùng LSN làm “bookmark” để track đến đâu đã process, tránh process duplicate hay bỏ sót changes.


Mid Level

Q: Giải thích sự khác biệt giữa fn_cdc_get_all_changes và fn_cdc_get_net_changes?

A:

  • get_all_changes: Trả về mọi operation riêng lẻ — nếu row UPDATE 5 lần sẽ thấy 10 records (5 before + 5 after). Dùng khi cần toàn bộ audit trail.
  • get_net_changes: Chỉ trả về trạng thái cuối cùng — nếu INSERT rồi UPDATE 5 lần sẽ thấy 1 INSERT với giá trị cuối. Yêu cầu @supports_net_changes = 1 khi enable. Dùng cho ETL batch loads.

Q: CDC overlap với log replication/Always On thế nào?

A: CDC đọc từ transaction log. Trên Always On secondary replicas chạy ở readable mode, CDC vẫn hoạt động bình thường trên primary. Log được ship sang secondary và CDC capture job chạy trên primary. Không nên chạy CDC capture trực tiếp từ secondary. Nếu failover xảy ra, new primary cần re-enable CDC jobs.


Senior Level

Q: Làm sao thiết kế một data integration pipeline dùng CDC để đảm bảo exactly-once semantics?

A: Exactly-once với CDC cần:

  1. Idempotent target operations: Dùng MERGE thay vì INSERT để handle duplicates. Nếu pipeline restart và replay từ cùng LSN, kết quả phải giống nhau.
  2. Transactional checkpoint: Lưu @to_lsn vào cùng transaction với data write vào target. Không dùng separate checkpoint store.
  3. Dead letter queue: Với rows không thể process, ghi vào DLQ thay vì skip hay crash.
  4. Version tracking: Trong target table, lưu source_lsnsource_seqval — kiểm tra trước khi apply change.
-- Idempotent merge với LSN tracking
MERGE dbo.Orders_DW AS target
USING (
    SELECT OrderID, CustomerID, Amount, __$start_lsn, __$seqval, __$operation
    FROM cdc.fn_cdc_get_all_changes_dbo_Orders(@from_lsn, @to_lsn, N'all update old')
    WHERE __$operation IN (2, 4)  -- INSERT, UPDATE after
) AS src ON target.OrderID = src.OrderID
    AND target.SourceLSN >= src.__$start_lsn  -- Đừng overwrite newer changes
WHEN MATCHED AND target.SourceLSN < src.__$start_lsn THEN 
    UPDATE SET CustomerID = src.CustomerID, Amount = src.Amount, SourceLSN = src.__$start_lsn
WHEN NOT MATCHED THEN 
    INSERT (OrderID, CustomerID, Amount, SourceLSN)
    VALUES (src.OrderID, src.CustomerID, src.Amount, src.__$start_lsn);

Q: Khi nào nên dùng Temporal Tables thay vì CDC? Liệu có thể dùng cả hai không?

A: Dùng Temporal Tables khi: Cần point-in-time queries trong application code (AS OF), audit trail đơn giản, không cần integration với external systems, muốn zero maintenance.

Dùng CDC khi: Cần feed data sang data warehouse/data lake, cần downstream systems nhận changes realtime/near-realtime, cần before values của deletes, complex ETL transformations.

Dùng cả hai: Hoàn toàn hợp lệ — Temporal Table cho audit/compliance, CDC cho ETL pipeline. Temporal history table có thể cũng được CDC capture (không phổ biến). Một pattern hay là: Temporal Table trên OLTP source → CDC feed từ source table → DW staging. Temporal table phục vụ point-in-time reporting trực tiếp từ OLTP khi cần.

C#/.NET

Giới thiệu

Tài liệu này tổng hợp các chủ đề quan trọng với vị trí C#/.NET Developer. Các câu hỏi được chia theo từng mảng kiến thức, từ nền tảng đến nâng cao.

Mục lục

#Mảng Kiến ThứcMô tả
1Nền Tảng C# và .NETNgôn ngữ C#, CLR, Garbage Collection, Value Types vs Reference Types
2ASP.NET Core Cốt lõiMiddleware, DI, Routing, Filters, Configuration
3Xây dựng Web APIRESTful, Authentication, Authorization, Swagger
4Truy cập Dữ liệu với EF CoreCode First, Migrations, N+1 Query, Transactions
5Kiến trúc Phần mềmSOLID, Design Patterns, Clean Architecture, DDD, CQRS
6Hiệu suất và Xử lý Bất đồng bộCaching, Rate Limiting, Load Balancing
7Hệ thống Phân tánMessage Queue, Docker, Kubernetes
8Kiểm thửUnit Test, Integration Test với xUnit
9Câu hỏi Phân biệtSo sánh các công nghệ và concepts

Hướng dẫn sử dụng

  1. Đọc theo thứ tự: Bắt đầu từ phần 1 (Nền tảng) và tiến dần đến các phần nâng cao
  2. Thực hành: Mỗi chủ đề cần có code example đi kèm
  3. Ôn tập lại

Note: Đây là tài liệu tổng hợp. Bạn có thể click vào từng chủ đề để xem chi tiết và bổ sung nội dung cụ thể.

1. Nền Tảng C# và .NET

Giới thiệu

Phần này trình bày các kiến thức nền tảng về ngôn ngữ C# và .NET Runtime, bao gồm:

Nội dung chính

C# Cơ bản

  • Cú pháp cơ bản - Kiểu dữ liệu, biến, toán tử, luồng điều khiển, phương thức

Lập trình hướng đối tượng

  • OOP - Class, Inheritance, Polymorphism, Encapsulation, Interface

Hệ thống kiểu & Generics

  • Types & Generics - Generic types, nullable types, type conversion, boxing/unboxing

Delegates, Events & Lambda

Collections

  • Collections - Arrays, List, Dictionary, HashSet và các collection interfaces

Xử lý chuỗi

  • Strings - String vs StringBuilder, string interpolation, string methods

Async/Await & LINQ

Exception Handling & IDisposable

Pattern Matching, Records & Reflection

CLR & Bộ nhớ


Câu hỏi phỏng vấn (Sắp xếp từ dễ đến khó)

Mức độ: Dễ (Junior)

  1. Sự khác biệt giữa value typesreference types trong C#?
  2. constreadonly khác nhau như thế nào?
  3. String vs StringBuilder - khi nào nên dùng cái nào?
  4. Sự khác biệt giữa for, while, và foreach loops?
  5. break, continue, và return khác nhau như thế nào?
  6. ArrayList<T> khác nhau gì?
  7. ref, out, in parameters khác nhau như thế nào?
  8. null-coalescing operator (??) và null-conditional operator (?.) dùng để làm gì?

Mức độ: Trung bình (Mid-level)

  1. abstract classinterface khác nhau như thế nào? Khi nào dùng cái nào?
  2. Giải thích inheritance, polymorphism, và encapsulation trong OOP?
  3. Sự khác biệt giữa delegate, event, và lambda expression?
  4. So sánh List<T>, Dictionary<TKey, TValue>, và HashSet<T> - khi nào dùng cái nào?
  5. Khi nào nên sử dụng generic types và cách áp dụng constraints?
  6. BoxingUnboxing là gì? Tại sao nên tránh?
  7. Deferred Execution trong LINQ là gì?
  8. Sự khác biệt giữa IEnumerableIQueryable - khi nào dùng cái nào?
  9. Giải thích try-catch-finally hoạt động thế nào? throwthrow ex khác nhau gì?
  10. IDisposable là gì? Tại sao cần implement nó?
  11. using statement hoạt động như thế nào với IDisposable?

Mức độ: Khó (Senior)

  1. Giải thích async/await hoạt động thế nào dưới the hood?
  2. Sự khác biệt giữa IEnumerableIAsyncEnumerable?
  3. Cách tối ưu truy vấn LINQ trên tập dữ liệu lớn?
  4. Garbage Collection (GC) hoạt động như thế nào?
  5. Các thế hệ (Generations) trong GC là gì?
  6. Cách tối ưu để giảm áp lực lên GC?
  7. Value Types (struct) vs Reference Types (class) – cách lưu trữ trên Stack/Heap và tác động đến hiệu năng?
  8. Reflection là gì và khi nào nên sử dụng? Performance considerations?
  9. Attributes trong C# dùng để làm gì? Cho ví dụ về custom attribute.
  10. Pattern Matching trong C# 9+ có gì mới?
  11. Records khác Classes như thế nào? Khi nào nên dùng Records?
  12. Tương tác giữa try-catch-finally và IDisposable - using statement tương đương với try-finally như thế nào?
  13. Tại sao không nên throw exception từ Dispose method?
  14. ConfigureAwait(false) dùng để làm gì? Khi nào nên sử dụng?

C# Cơ bản

Cú pháp cơ bản

Kiểu dữ liệu

C# có hai loại kiểu dữ liệu chính:

// Value types (lưu trên stack)
int number = 42;
double price = 19.99;
bool isActive = true;
char letter = 'A';
DateTime now = DateTime.Now;

// Reference types (lưu trên heap)
string name = "John";
object obj = new object();
int[] numbers = new int[] { 1, 2, 3 };

Biến và Hằng số

// Biến thông thường
var message = "Hello"; // Type inference
string name = "Alice";

// Hằng số
const int MaxUsers = 100;
const string AppName = "MyApp";

// Readonly (chỉ gán trong constructor)
readonly string _connectionString;

Toán tử

// Toán tử số học
int sum = 5 + 3;      // 8
int diff = 10 - 4;    // 6
int product = 6 * 7;  // 42
int quotient = 15 / 3; // 5
int remainder = 10 % 3; // 1

// Toán tử so sánh
bool isEqual = (5 == 5);      // true
bool notEqual = (5 != 3);     // true
bool greaterThan = (10 > 5);  // true
bool lessThan = (3 < 7);      // true

// Toán tử logic
bool and = (true && false);   // false
bool or = (true || false);    // true
bool not = !true;             // false

// Toán tử ternary
int score = 85;
string grade = score >= 60 ? "Pass" : "Fail"; // "Pass"

Luồng điều khiển

// if-else
if (age >= 18)
{
    Console.WriteLine("Adult");
}
else if (age >= 13)
{
    Console.WriteLine("Teenager");
}
else
{
    Console.WriteLine("Child");
}

// switch
string day = "Monday";
switch (day)
{
    case "Monday":
        Console.WriteLine("Start of week");
        break;
    case "Friday":
        Console.WriteLine("Weekend is near");
        break;
    default:
        Console.WriteLine("Midweek");
        break;
}

// Vòng lặp
for (int i = 0; i < 10; i++)
{
    Console.WriteLine(i);
}

int count = 0;
while (count < 5)
{
    Console.WriteLine(count);
    count++;
}

do
{
    Console.WriteLine("At least once");
} while (false);

// foreach
string[] names = { "Alice", "Bob", "Charlie" };
foreach (string name in names)
{
    Console.WriteLine(name);
}

Phương thức

// Method với parameters và return type
public int Add(int a, int b)
{
    return a + b;
}

// Optional parameters
public void Greet(string name = "Guest")
{
    Console.WriteLine($"Hello, {name}!");
}

// Method overloading
public int Multiply(int a, int b) => a * b;
public double Multiply(double a, double b) => a * b;

// ref, out, in parameters
public void Swap(ref int a, ref int b)
{
    int temp = a;
    a = b;
    b = temp;
}

public bool TryParse(string input, out int result)
{
    return int.TryParse(input, out result);
}

public double Calculate(in double value)
{
    // value cannot be modified
    return value * 2;
}

Lập trình hướng đối tượng (OOP)

Class và Object

public class Person
{
    // Fields
    private string _name;
    private int _age;
    
    // Properties
    public string Name
    {
        get => _name;
        set => _name = value;
    }
    
    public int Age
    {
        get => _age;
        set
        {
            if (value >= 0)
                _age = value;
        }
    }
    
    // Auto-property
    public string Email { get; set; }
    
    // Constructor
    public Person(string name, int age)
    {
        _name = name;
        _age = age;
    }
    
    // Method
    public void Introduce()
    {
        Console.WriteLine($"Hi, I'm {_name}, {_age} years old.");
    }
}

// Sử dụng
var person = new Person("Alice", 30);
person.Introduce();

Inheritance (Kế thừa)

public class Animal
{
    public string Name { get; set; }
    
    public virtual void MakeSound()
    {
        Console.WriteLine("Some sound");
    }
}

public class Dog : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine("Woof!");
    }
    
    public void Fetch()
    {
        Console.WriteLine("Fetching ball...");
    }
}

// Sử dụng
Animal myDog = new Dog();
myDog.MakeSound(); // "Woof!"

Polymorphism (Đa hình)

public abstract class Shape
{
    public abstract double CalculateArea();
}

public class Circle : Shape
{
    public double Radius { get; set; }
    
    public override double CalculateArea()
    {
        return Math.PI * Radius * Radius;
    }
}

public class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }
    
    public override double CalculateArea()
    {
        return Width * Height;
    }
}

// Sử dụng đa hình
List<Shape> shapes = new List<Shape>
{
    new Circle { Radius = 5 },
    new Rectangle { Width = 4, Height = 6 }
};

foreach (var shape in shapes)
{
    Console.WriteLine($"Area: {shape.CalculateArea()}");
}

Encapsulation (Đóng gói)

public class BankAccount
{
    // Private field - encapsulated
    private decimal _balance;
    
    // Public property với validation
    public decimal Balance
    {
        get => _balance;
        private set
        {
            if (value >= 0)
                _balance = value;
        }
    }
    
    public void Deposit(decimal amount)
    {
        if (amount > 0)
            Balance += amount;
    }
    
    public bool Withdraw(decimal amount)
    {
        if (amount > 0 && amount <= Balance)
        {
            Balance -= amount;
            return true;
        }
        return false;
    }
}

Interface

public interface ILogger
{
    void Log(string message);
    void LogError(string error);
}

public interface IDisposable
{
    void Dispose();
}

public class FileLogger : ILogger, IDisposable
{
    public void Log(string message)
    {
        File.AppendAllText("log.txt", $"{DateTime.Now}: {message}\n");
    }
    
    public void LogError(string error)
    {
        Log($"ERROR: {error}");
    }
    
    public void Dispose()
    {
        // Cleanup resources
    }
}

Abstract Class vs Interface

FeatureAbstract ClassInterface
Constructor✅ Có❌ Không
Fields✅ Có❌ Không (chỉ properties)
Method implementation✅ Có (có thể có cả abstract và concrete)❌ Không (C# 8+ có default implementation)
Multiple inheritance❌ Không✅ Có
Access modifiers✅ Có (public, protected, private)❌ Mặc định public

Hệ thống kiểu & Generics

Generics

// Generic class
public class Repository<T> where T : class
{
    private List<T> _items = new List<T>();
    
    public void Add(T item)
    {
        _items.Add(item);
    }
    
    public T GetById(int id)
    {
        // Implementation
        return default(T);
    }
}

// Generic method
public T Max<T>(T a, T b) where T : IComparable<T>
{
    return a.CompareTo(b) > 0 ? a : b;
}

// Sử dụng
var repo = new Repository<Product>();
repo.Add(new Product());

int max = Max(10, 20); // 20

Nullable Types

// Nullable value types
int? nullableInt = null;
DateTime? nullableDate = null;

if (nullableInt.HasValue)
{
    int value = nullableInt.Value;
}

// Null-coalescing operator
int safeValue = nullableInt ?? 0;

// Null-conditional operator
string name = person?.Name?.ToUpper() ?? "Unknown";

// Null-forgiving operator (C# 8+)
string notNull = possiblyNull!; // Assert not null

Type Conversion

// Implicit conversion
int small = 10;
long large = small; // OK

// Explicit conversion (cast)
double d = 9.8;
int i = (int)d; // 9

// as operator (returns null if fails)
object obj = "Hello";
string str = obj as string; // "Hello"
int? number = obj as int?; // null

// is operator
if (obj is string)
{
    Console.WriteLine("It's a string");
}

// Pattern matching with is
if (obj is string s)
{
    Console.WriteLine($"Length: {s.Length}");
}

Boxing và Unboxing

// Boxing - value type → reference type
int value = 42;
object boxed = value; // Boxing

// Unboxing - reference type → value type
int unboxed = (int)boxed; // Unboxing

// Performance impact
// Boxing allocates memory on heap
// Unboxing requires type check and copy

Delegates, Events & Lambda

Delegates

// Delegate declaration
public delegate void Notify(string message);

// Delegate instance
Notify notifier = SendEmail;

// Multicast delegate
notifier += SendSMS;
notifier += LogMessage;

// Invoke
notifier("Hello!");

// Built-in delegates
Action<string> action = Console.WriteLine;
Func<int, int, int> add = (a, b) => a + b;
Predicate<int> isEven = n => n % 2 == 0;

void SendEmail(string msg) { /* ... */ }
void SendSMS(string msg) { /* ... */ }
void LogMessage(string msg) { /* ... */ }

Events

public class Button
{
    // Event declaration
    public event EventHandler Clicked;
    
    public void Click()
    {
        // Raise event
        Clicked?.Invoke(this, EventArgs.Empty);
    }
}

// Sử dụng
var button = new Button();
button.Clicked += (sender, e) => Console.WriteLine("Button clicked!");

// Event với custom EventArgs
public class OrderPlacedEventArgs : EventArgs
{
    public int OrderId { get; set; }
    public decimal Total { get; set; }
}

public class OrderService
{
    public event EventHandler<OrderPlacedEventArgs> OrderPlaced;
    
    public void PlaceOrder(Order order)
    {
        // Process order
        OrderPlaced?.Invoke(this, new OrderPlacedEventArgs
        {
            OrderId = order.Id,
            Total = order.Total
        });
    }
}

Lambda Expressions

// Expression lambda
Func<int, int> square = x => x * x;

// Statement lambda
Action<int> print = x =>
{
    Console.WriteLine($"Value: {x}");
    Console.WriteLine($"Square: {x * x}");
};

// Lambda với multiple parameters
Func<int, int, int> multiply = (x, y) => x * y;

// Sử dụng trong LINQ
var evenNumbers = numbers.Where(n => n % 2 == 0);

Mối quan hệ giữa Delegate, Event và Lambda Expression

Ba khái niệm này có mối quan hệ chặt chẽ và thường bị nhầm lẫn. Hiểu được bản chất của chúng sẽ giúp bạn sử dụng đúng cách.

Bản chất

Khái niệmBản chấtVai trò
DelegateKiểu dữ liệu (type-safe function pointer)Định nghĩa “hợp đồng” cho phương thức
Lambda ExpressionCú pháp (syntax)Cách viết ngắn gọn cho anonymous method
EventCơ chế bảo vệ (wrapper)Giới hạn truy cập delegate, chỉ cho phép +=-=

Mối quan hệ

┌─────────────────────────────────────────────────────────────┐
│                     Mối quan hệ                              │
│                                                             │
│   Lambda Expression ──► tạo ra ──► Delegate Instance         │
│                              │                              │
│                              ▼                              │
│   Event ──► bao bọc (wraps) ──► Delegate Field              │
│                                                             │
│   Kết quả: Event + Lambda = Pattern phổ biến trong C#       │
└─────────────────────────────────────────────────────────────┘

Giải thích chi tiết

1. Lambda Expression thực chất là Delegate

Khi bạn viết một lambda expression, compiler sẽ chuyển nó thành delegate:

// Lambda expression
Func<int, int> square = x => x * x;

// Compiler tạo ra tương đương:
Func<int, int> square = delegate(int x) { return x * x; };

// Hoặc thậm chí là một phương thức private:
// private static int <Main>b__0_0(int x) { return x * x; }

2. Event là “wrapper” bảo vệ cho Delegate

Event không phải là một kiểu riêng biệt - nó là một delegate được bảo vệ:

public class Publisher
{
    // Delegate field (private)
    private EventHandler _clicked;
    
    // Event - chỉ cho phép += và -= từ bên ngoài
    public event EventHandler Clicked
    {
        add { _clicked += value; }
        remove { _clicked -= value; }
    }
    
    // Bên trong class, có thể Invoke
    public void Raise() => _clicked?.Invoke(this, EventArgs.Empty);
}

3. Lambda + Event = Pattern phổ biến

// Lambda expression được dùng để tạo delegate handler
button.Clicked += (sender, e) => Console.WriteLine("Clicked!");
//                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//                Lambda expression → EventHandler delegate

Ví dụ tổng hợp

// 1. Định nghĩa delegate (kiểu)
public delegate void MessageHandler(string message);

public class ChatRoom
{
    // 2. Event bao bọc delegate
    public event MessageHandler MessageReceived;
    
    public void SendMessage(string from, string message)
    {
        // 3. Lambda expression tạo delegate instance để invoke
        MessageReceived?.Invoke($"{from}: {message}");
    }
}

// 4. Sử dụng lambda để subscribe event
var chat = new ChatRoom();
chat.MessageReceived += msg => Console.WriteLine(msg);
//                           ^^^^^^^^^^^^^^^^^^^^^^^^^^
//                           Lambda → MessageHandler delegate

chat.SendMessage("Alice", "Hello!");

Khi nào dùng gì?

Tình huốngSử dụng
Truyền method như parameterFunc<>, Action<>, hoặc custom delegate
LINQ queriesLambda expression
Callback không cần exposeLambda → Delegate
Publisher-Subscriber patternEvent
Cần giới hạn truy cập (chỉ +=/-=)Event
Cần Invoke từ bên ngoàiDelegate (không dùng event)

Lưu ý quan trọng

  1. Event không thể Invoke từ bên ngoài class - Đây là sự khác biệt chính so với delegate
  2. Lambda expression không có kiểu riêng - Nó phải được gán cho một delegate type
  3. Event tự động generate add/remove - Tương tự property tự động generate getter/setter

Collections

Arrays

// Single-dimensional array
int[] numbers = new int[5];
numbers[0] = 1;

int[] initialized = { 1, 2, 3, 4, 5 };

// Multi-dimensional array
int[,] matrix = new int[3, 3];
matrix[0, 0] = 1;

// Jagged array (array of arrays)
int[][] jagged = new int[3][];
jagged[0] = new int[] { 1, 2, 3 };

List

List<string> names = new List<string>();
names.Add("Alice");
names.Add("Bob");
names.Add("Charlie");

// Initialization
var numbers = new List<int> { 1, 2, 3, 4, 5 };

// Common operations
bool contains = names.Contains("Alice"); // true
int index = names.IndexOf("Bob"); // 1
names.Remove("Charlie");
names.Sort();

// Capacity management
names.Capacity = 100; // Pre-allocate
names.TrimExcess(); // Reduce capacity

Dictionary<TKey, TValue>

Dictionary<string, int> ages = new Dictionary<string, int>();
ages["Alice"] = 30;
ages["Bob"] = 25;

// Safe access
if (ages.TryGetValue("Alice", out int age))
{
    Console.WriteLine($"Alice is {age} years old");
}

// Initialization
var scores = new Dictionary<string, int>
{
    ["Alice"] = 95,
    ["Bob"] = 87,
    ["Charlie"] = 92
};

// Iteration
foreach (var kvp in ages)
{
    Console.WriteLine($"{kvp.Key}: {kvp.Value}");
}

HashSet

HashSet<int> uniqueNumbers = new HashSet<int>();
uniqueNumbers.Add(1);
uniqueNumbers.Add(2);
uniqueNumbers.Add(1); // Duplicate - ignored

// Set operations
var set1 = new HashSet<int> { 1, 2, 3 };
var set2 = new HashSet<int> { 3, 4, 5 };

set1.UnionWith(set2); // {1, 2, 3, 4, 5}
set1.IntersectWith(set2); // {3}
set1.ExceptWith(set2); // {1, 2}

Queue và Stack

// Queue (FIFO)
Queue<string> queue = new Queue<string>();
queue.Enqueue("First");
queue.Enqueue("Second");
string first = queue.Dequeue(); // "First"

// Stack (LIFO)
Stack<string> stack = new Stack<string>();
stack.Push("First");
stack.Push("Second");
string last = stack.Pop(); // "Second"

Collection Interfaces

InterfaceMô tảVí dụ
IEnumerable<T>Chỉ hỗ trợ iterationLINQ queries
ICollection<T>Thêm Count, Add, Remove, ClearList<T>, HashSet<T>
IList<T>Thêm index-based accessList<T>, arrays
IDictionary<TKey, TValue>Key-value pairsDictionary<TKey, TValue>
IReadOnlyCollection<T>Chỉ đọc với CountIEnumerable<T>.ToList()

Span và Memory - High-Performance Memory Views

Lưu ý: Span<T>Memory<T> không phải là collections theo nghĩa truyền thống. Chúng là các ref struct cung cấp view an toàn, zero-allocation trên bộ nhớ liên tục.

Span là gì?

Span<T> là một stack-only type (ref struct) cho phép truy cập an toàn vào một vùng bộ nhớ liên tục mà không cần allocation trên heap.

// Tạo Span từ array
int[] array = { 1, 2, 3, 4, 5 };
Span<int> span = array.AsSpan();

// Slice - tạo view mà không copy dữ liệu
Span<int> slice = span[1..3]; // { 2, 3 }

// Truy cập phần tử
span[0] = 10; // array[0] cũng thay đổi

// Tạo Span từ stack
Span<int> stackSpan = stackalloc int[3] { 1, 2, 3 };

// Tạo Span từ string (readonly)
ReadOnlySpan<char> text = "Hello World".AsSpan();

Span vs Collections

Đặc điểmSpan<T>Collections (List<T>, T[])
Kiểuref struct (stack-only)Reference type hoặc array
AllocationZero heap allocationHeap allocation
Lưu trữKhông thể là field của classCó thể là field
AsyncKhông hỗ trợ (ref struct limitation)Hỗ trợ đầy đủ
IEnumerableKhông implementCó implement
PerformanceCao nhấtThấp hơn do allocation
Use caseParsing, slicing, buffer manipulationGeneral purpose storage

Memory - Phiên bản linh hoạt hơn

Memory<T> không phải ref struct nên có thể lưu trữ trong class và sử dụng với async:

public class DataProcessor
{
    // Memory<T> có thể là field của class
    private Memory<byte> _buffer;
    
    public DataProcessor(Memory<byte> buffer)
    {
        _buffer = buffer;
    }
    
    // Có thể dùng với async
    public async Task ProcessAsync()
    {
        // Span không thể dùng trong async method
        Span<byte> span = _buffer.Span;
        // ... process
    }
}

// Sử dụng
var memory = new Memory<byte>(new byte[1024]);
var span = memory.Span; // Lấy Span từ Memory

Khi nào sử dụng Span và Memory?

Tình huốngKhuyến nghị
Parsing string/byte bufferSpan<T>
Slicing array mà không copySpan<T>
High-performance buffer manipulationSpan<T>
Cần lưu trữ trong classMemory<T>
Cần sử dụng với async/awaitMemory<T>
General purpose data storageList<T>, T[]

Ví dụ thực tế: Parsing với Span

// Không allocation khi parsing
int ParseNumbers(ReadOnlySpan<char> input)
{
    int sum = 0;
    while (!input.IsEmpty)
    {
        int commaIndex = input.IndexOf(',');
        if (commaIndex == -1)
        {
            sum += int.Parse(input);
            break;
        }
        
        sum += int.Parse(input[..commaIndex]);
        input = input[(commaIndex + 1)..];
    }
    return sum;
}

// Sử dụng
var csv = "1,2,3,4,5";
int total = ParseNumbers(csv.AsSpan()); // total = 15

Tại sao Span không phải là Collection?

  1. Không implement IEnumerable<T> - Không thể dùng foreach trực tiếp (phải dùng foreach(ref var item in span))
  2. Không implement ICollection<T> - Không có Add, Remove, Clear
  3. Là view, không phải storage - Span<T> không sở hữu dữ liệu, nó chỉ “nhìn” vào vùng bộ nhớ có sẵn
  4. Stack-only - Không thể boxing, không thể lưu trong heap

Tóm lại: Span<T>Memory<T>memory views, không phải collections. Chúng bổ sung cho collections bằng cách cung cấp hiệu suất cao cho các thao tác trên bộ nhớ liên tục.

Xử lý chuỗi

String vs StringBuilder

// String - immutable
string s1 = "Hello";
string s2 = s1 + " World"; // Tạo string mới

// StringBuilder - mutable (hiệu quả cho nhiều thao tác)
StringBuilder sb = new StringBuilder();
sb.Append("Hello");
sb.Append(" ");
sb.Append("World");
string result = sb.ToString(); // "Hello World"

// Khi nào dùng StringBuilder?
// - Nhiều string concatenations trong loop
// - Xây dựng string động từ nhiều phần
// - Hiệu suất quan trọng

String Interpolation

string name = "Alice";
int age = 30;

// String interpolation (C# 6+)
string message = $"Hello, {name}. You are {age} years old.";

// Format specifiers
decimal price = 19.99m;
string formatted = $"Price: {price:C}"; // "Price: $19.99"
string percent = $"Discount: {0.15:P}"; // "Discount: 15.00%"

// Expression trong interpolation
string status = $"User is {(age >= 18 ? "adult" : "minor")}";

String Methods

string text = "  Hello World  ";

// Common operations
string trimmed = text.Trim(); // "Hello World"
string upper = text.ToUpper(); // "  HELLO WORLD  "
string lower = text.ToLower(); // "  hello world  "

// Searching
bool contains = text.Contains("Hello"); // true
int index = text.IndexOf("World"); // 9
bool startsWith = text.StartsWith("  Hello"); // true
bool endsWith = text.EndsWith("World  "); // true

// Splitting và joining
string[] parts = "a,b,c".Split(',');
string joined = string.Join("-", parts); // "a-b-c"

// Formatting
string formatted = string.Format("{0} is {1} years old", name, age);

Async/Await & Collections Nâng cao

Async/Await

Cơ chế hoạt động

async/await là cú pháp cho phép viết code bất đồng bộ một cách đồng bộ (synchronous-looking).

public async Task<string> FetchDataAsync()
{
    using var client = new HttpClient();
    var result = await client.GetStringAsync("https://api.example.com/data");
    return result;
}

Luồng xử lý

  1. Khi gọi method async, thread hiện tại không bị block
  2. Task được tạo và chạy trên Thread Pool
  3. Khi async operation hoàn thành, continuation được schedule lại
  4. Kết quả được trả về cho caller

Lưu ý quan trọng

  • async method luôn trả về Task, Task<T>, hoặc void (chỉ dùng cho event handlers)
  • Không nên dùng .Result hoặc .Wait() vì sẽ gây deadlock
  • Sử dụng ConfigureAwait(false) để tránh context capture

IEnumerable vs IAsyncEnumerable

IEnumerable

public IEnumerable<Product> GetProducts()
{
    return _context.Products; // Deferred execution
}
  • Synchronous - load all data vào memory
  • Deferred Execution - không thực thi cho đến khi được enumerate
  • Phù hợp cho tập dữ liệu nhỏ

IAsyncEnumerable (.NET Core 2.1+)

public async IAsyncEnumerable<Product> GetProductsAsync()
{
    await foreach (var product in _context.Products.AsAsyncEnumerable())
    {
        yield return product;
    }
}
  • Asynchronous - stream data từ database
  • Non-blocking - không block thread
  • Phù hợp cho tập dữ liệu lớn hoặc real-time streaming

So sánh

FeatureIEnumerableIAsyncEnumerable
ExecutionSynchronousAsynchronous
MemoryLoad allStream
PerformanceChậm với large dataTốt với large data
Use caseSmall datasetsLarge datasets, real-time

LINQ

Deferred Execution

var query = products.Where(p => p.Price > 100); // Chưa execute
var result = query.ToList(); // Execute tại đây
  • Query chỉ được thực thi khi:
    • Gọi .ToList(), .ToArray(), .Count(), .First(), etc.
    • Sử dụng foreach

IEnumerable vs IQueryable

// IEnumerable - Thực thi trong memory
IEnumerable<Product> products = _context.Products.ToList();
var filtered = products.Where(p => p.Price > 100); // Filter in memory

// IQueryable - Thực thi trên database
IQueryable<Product> query = _context.Products;
var filteredQuery = query.Where(p => p.Price > 100); // Filter in SQL
FeatureIEnumerableIQueryable
ExecutionIn-memoryDatabase/Provider
Deferred Execution✅ Yes✅ Yes
Query CompositionLINQ to ObjectsLINQ to Entities/SQL
PerformanceLoad all data firstFilter at database
Use caseSmall datasets, in-memoryDatabase queries, large datasets

Khi nào dùng IQueryable?

// ✅ Tốt - Filter ở database
var expensiveProducts = _context.Products
    .Where(p => p.Price > 100)
    .ToList(); // SQL: SELECT * FROM Products WHERE Price > 100

// ❌ Không tốt - Load all rồi filter
var allProducts = _context.Products.ToList(); // SELECT * FROM Products
var expensive = allProducts.Where(p => p.Price > 100); // Filter in memory

Tối ưu LINQ trên tập dữ liệu lớn

  1. Sử dụng IQueryable thay vì IEnumerable cho database queries

    public IQueryable<Product> GetProducts() => _context.Products;
    
  2. Avoid multiple enumerations

    // Bad - Multiple enumerations (deferred execution)
    var query = products.Where(x => x.Active); // IEnumerable, chưa execute
    var count = query.Count();  // First enumeration - executes query
    var list = query.ToList();  // Second enumeration - executes query AGAIN!
    
    // Good - Materialize once
    var list = products.Where(x => x.Active).ToList(); // Materialize once
    var count = list.Count; // Use cached list, no re-enumeration
    
  3. Use pagination

    Offset-based pagination (phù hợp cho admin dashboard, báo cáo):

    // ✅ Offset-based - đơn giản, dễ implement
    var page = products
        .OrderBy(p => p.Id)
        .Skip((pageNumber - 1) * pageSize)
        .Take(pageSize)
        .ToList();
    

    Cursor-based pagination (phù hợp cho infinite scroll, mobile app, real-time feed):

    // ✅ Cursor-based - hiệu suất cao, không bị "drift" khi data thay đổi
    // Client gửi cursor = Id của item cuối cùng đã load
    var page = products
        .Where(p => p.Id < lastSeenId) // hoặc > nếu sort ascending
        .OrderByDescending(p => p.Id)
        .Take(pageSize)
        .ToList();
    
    // Trả về cursor cho client (Id của item cuối cùng)
    var nextCursor = page.LastOrDefault()?.Id;
    
    LoạiKhi nào dùngƯu điểmNhược điểm
    Offset-basedAdmin dashboard, báo cáo, cần nhảy đến page cụ thểĐơn giản, dễ implement, hỗ trợ “jump to page N”Chậm với large offset, bị “drift” khi data thay đổi
    Cursor-basedInfinite scroll, mobile app, real-time feed (Facebook, Twitter)Hiệu suất cao (dùng index), không bị driftKhông nhảy page được, chỉ “next/prev”
  4. Select only needed columns

    var names = products.Select(p => p.Name).ToList(); // Not full entity
    
  5. Eager loading vs Lazy loading

    // Lazy loading (nhiều queries)
    var products = context.Products.ToList();
    foreach (var p in products)
    {
        var category = p.Category; // Additional query mỗi lần
    }
    
    // Eager loading (1 query với join)
    var products = context.Products
        .Include(p => p.Category)
        .ToList();
    

Exception Handling & IDisposable

Try-Catch-Finally

Cấu trúc cơ bản

try
{
    // Code có thể throw exception
    var result = PerformOperation();
}
catch (ArgumentException ex)
{
    // Handle specific exception
    Log.Error(ex, "Invalid argument");
    throw; // Re-throw với original stack trace
}
catch (Exception ex)
{
    // Handle general exception
    throw new CustomException("Error occurred", ex); // Wrap exception
}
finally
{
    // Cleanup - luôn chạy dù có exception hay không
    Cleanup();
}

Finally block luôn được thực thi

try
{
    Console.WriteLine("In try block");
    return;
}
finally
{
    Console.WriteLine("In finally block"); // Vẫn được thực thi!
}
// Output:
// In try block
// In finally block

Multiple Catch Blocks

try
{
    // Risky operation
}
catch (FileNotFoundException ex)
{
    // Handle file not found
    Console.WriteLine($"File missing: {ex.FileName}");
}
catch (IOException ex)
{
    // Handle IO errors
    Console.WriteLine($"IO error: {ex.Message}");
}
catch (Exception ex)
{
    // Handle all other exceptions
    Console.WriteLine($"Unexpected error: {ex.Message}");
}

Exception Filters (C# 6+)

try
{
    // Risky operation
}
catch (Exception ex) when (ex is IOException)
{
    // Handle IO exceptions
}
catch (Exception ex) when (ex.Message.Contains("timeout"))
{
    // Handle timeout specifically
}

Anti-patterns cần tránh

// ❌ Bad - Swallow exception
catch (Exception ex)
{
    // Do nothing
}

// ✅ Good - Always log or handle
catch (Exception ex)
{
    Log.Error(ex);
    throw;
}

// ❌ Bad - Lose stack trace
catch (Exception ex)
{
    throw ex; // Resets stack trace
}

// ✅ Good - Preserve stack trace
catch (Exception ex)
{
    throw; // Preserves stack trace
}

Tại sao throw ex reset stack trace?

Khi sử dụng throw ex, CLR (Common Language Runtime) coi đây là một exception mới được ném từ vị trí hiện tại, thay vì tiếp tục truyền exception gốc lên trên. Điều này dẫn đến việc stack trace bị reset và chỉ bắt đầu từ vị trí throw ex.

Cơ chế hoạt động

void MethodA()
{
    MethodB();
}

void MethodB()
{
    MethodC();
}

void MethodC()
{
    throw new InvalidOperationException("Error in C");
}

try
{
    MethodA();
}
catch (Exception ex)
{
    // ❌ throw ex - Stack trace bị reset
    throw ex;
    
    // Stack trace chỉ còn:
    // at Program.Main() in Program.cs:line XX
    // (Mất thông tin MethodA -> MethodB -> MethodC)
}
try
{
    MethodA();
}
catch (Exception ex)
{
    // ✅ throw - Stack trace được giữ nguyên
    throw;
    
    // Stack trace đầy đủ:
    // at Program.MethodC() in Program.cs:line XX
    // at Program.MethodB() in Program.cs:line XX
    // at Program.MethodA() in Program.cs:line XX
    // at Program.Main() in Program.cs:line XX
}

Giải thích chi tiết

Aspectthrow exthrow
Stack traceBị reset từ vị trí throwGiữ nguyên stack trace gốc
Exception objectTạo mới (về mặt stack trace)Giữ nguyên object gốc
DebuggingKhó tìm root causeDễ dàng trace ngược lại
IL Codethrow instructionrethrow instruction

Khi nào dùng throw ex?

Thực tế, hầu như không nên dùng throw ex. Tuy nhiên, có một số trường hợp đặc biệt:

// Trường hợp duy nhất chấp nhận được:
// Khi bạn muốn wrap exception vào một exception khác
catch (Exception ex)
{
    throw new CustomException("Context-specific message", ex);
    // InnerException giữ nguyên stack trace gốc
}

ExceptionDispatchInfo - Giữ stack trace khi re-throw

Nếu bạn cần re-throw exception ở một vị trí khác (ví dụ: trong async scenarios), sử dụng ExceptionDispatchInfo:

using System.Runtime.ExceptionServices;

ExceptionDispatchInfo capturedException = null;

try
{
    // Some operation
}
catch (Exception ex)
{
    capturedException = ExceptionDispatchInfo.Capture(ex);
}

// Later, possibly in a different location
capturedException?.Throw();
// Stack trace vẫn được giữ nguyên!

Rule of thumb: Luôn sử dụng throw; thay vì throw ex; để giữ nguyên stack trace gốc, giúp việc debugging dễ dàng hơn.


IDisposable Interface

Interface Definition

public interface IDisposable
{
    void Dispose();
}

Implementing IDisposable

public class ResourceHolder : IDisposable
{
    private bool _disposed = false;
    private FileStream _fileStream;
    
    public ResourceHolder(string path)
    {
        _fileStream = new FileStream(path, FileMode.Open);
    }
    
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
    
    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                // Dispose managed resources
                _fileStream?.Dispose();
            }
            
            // Free unmanaged resources
            // (if any)
            
            _disposed = true;
        }
    }
    
    ~ResourceHolder()
    {
        Dispose(false);
    }
}

Using Statement

// Traditional using statement
using (var stream = new FileStream("file.txt", FileMode.Open))
{
    // Use stream
} // Dispose() called automatically

// C# 8+ using declaration
using var stream = new FileStream("file.txt", FileMode.Open);
// Use stream
// Dispose() called at end of scope

Tương tác giữa Try-Catch-Finally và IDisposable

Using vs Try-Finally

// Using statement (recommended)
using var resource = new ResourceHolder("file.txt");
resource.DoWork();
// Dispose() called automatically, even if exception occurs

// Equivalent try-finally
ResourceHolder resource = null;
try
{
    resource = new ResourceHolder("file.txt");
    resource.DoWork();
}
finally
{
    resource?.Dispose();
}

Multiple Resources với Using

// Multiple using statements
using var file1 = new FileStream("file1.txt", FileMode.Open);
using var file2 = new FileStream("file2.txt", FileMode.Open);
using var reader = new StreamReader(file1);
using var writer = new StreamWriter(file2);

// All resources disposed in reverse order

Exception trong Dispose

public class SafeResource : IDisposable
{
    private bool _disposed = false;
    
    public void Dispose()
    {
        try
        {
            Dispose(true);
        }
        catch
        {
            // Log but don't throw - Dispose should not throw
            Log.Error("Error during dispose");
        }
        finally
        {
            GC.SuppressFinalize(this);
            _disposed = true;
        }
    }
    
    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            // Cleanup managed resources
        }
    }
}

Best Practices

  1. Luôn sử dụng using cho IDisposable resources

    // ✅ Good
    using var connection = new SqlConnection(connectionString);
    connection.Open();
    
    // ❌ Bad
    var connection = new SqlConnection(connectionString);
    connection.Open();
    // Forgot to dispose!
    
  2. Không throw exception từ Dispose

    public void Dispose()
    {
        try
        {
            Cleanup();
        }
        catch (Exception ex)
        {
            Log.Error(ex); // Log, don't throw
        }
    }
    
  3. Implement Dispose pattern đúng cách

    public class MyResource : IDisposable
    {
        private bool _disposed = false;
        
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        
        protected virtual void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                if (disposing)
                {
                    // Dispose managed resources
                }
                // Free unmanaged resources
                _disposed = true;
            }
        }
    }
    
  4. Kết hợp try-catch với using

    try
    {
        using var resource = new ResourceHolder("file.txt");
        resource.DoWork();
    }
    catch (IOException ex)
    {
        Log.Error(ex, "IO error during operation");
        throw;
    }
    // resource.Dispose() called automatically
    

Khi nào implement IDisposable?

  • Khi class sử dụng unmanaged resources (file handles, database connections, etc.)
  • Khi class chứa các IDisposable members
  • Khi cần deterministic cleanup

Khi nào KHÔNG cần IDisposable?

  • Chỉ sử dụng managed resources
  • Không cần cleanup đặc biệt
  • Object lifetime ngắn và GC có thể handle

Pattern Matching, Records & Reflection

Pattern Matching

C# 9+ Pattern Matching

// Type pattern
if (obj is Product product)
{
    Console.WriteLine(product.Name);
}

// Switch expression
var message = obj switch
{
    null => "Empty",
    int i when i > 0 => "Positive",
    int i => "Negative",
    string s => $"String: {s}",
    _ => "Unknown"
};

// Relational patterns (C# 9)
var category = score switch
{
    >= 90 => "A",
    >= 80 => "B",
    >= 70 => "C",
    _ => "F"
};

Records (C# 9+)

Value Equality

public record Product(string Name, decimal Price);

// So sánh bằng giá trị, không phải reference
var p1 = new Product("Apple", 1.99m);
var p2 = new Product("Apple", 1.99m);

Console.WriteLine(p1 == p2); // True!

With Expression

var p1 = new Product("Apple", 1.99m);
var p2 = p1 with { Price = 2.99m }; // Tạo bản sao với Price mới

// Non-destructive mutation

Positional Records

public record Product(string Name, decimal Price);

// Constructor và Deconstruct tự động
var product = new Product("Apple", 1.99m);
var (name, price) = product;

Attributes & Reflection

Attributes (Thuộc tính)

Attributes cung cấp metadata cho code elements (classes, methods, properties, etc.).

// Built-in attributes
[Serializable]
public class Product { }

[Obsolete("Use NewMethod instead")]
public void OldMethod() { }

[Conditional("DEBUG")]
public void DebugLog(string message) { }

// Custom attribute
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AuthorAttribute : Attribute
{
    public string Name { get; }
    public string Version { get; set; }
    
    public AuthorAttribute(string name)
    {
        Name = name;
    }
}

// Sử dụng custom attribute
[Author("John Doe", Version = "1.0")]
public class Calculator
{
    [Author("Jane Smith")]
    public int Add(int a, int b) => a + b;
}

Common Built-in Attributes

AttributeMục đíchVí dụ
[Serializable]Cho phép object serialization[Serializable] class Data
[Obsolete]Đánh dấu method/class deprecated[Obsolete("Use v2")]
[Conditional]Chỉ compile khi symbol defined[Conditional("DEBUG")]
[DllImport]Import native DLL function[DllImport("user32.dll")]
[Required]Data validation (ASP.NET Core)[Required] string Name
[Range]Value range validation[Range(1, 100)] int Age

Reflection

Reflection cho phép inspect và thao tác với types tại runtime.

using System.Reflection;

// Lấy Type information
Type type = typeof(Product);
Console.WriteLine($"Type: {type.Name}");
Console.WriteLine($"Namespace: {type.Namespace}");
Console.WriteLine($"Is Class: {type.IsClass}");

// Lấy properties
PropertyInfo[] properties = type.GetProperties();
foreach (var prop in properties)
{
    Console.WriteLine($"Property: {prop.Name} ({prop.PropertyType.Name})");
}

// Lấy methods
MethodInfo[] methods = type.GetMethods();
foreach (var method in methods)
{
    Console.WriteLine($"Method: {method.Name}");
}

// Kiểm tra attributes
var attributes = type.GetCustomAttributes(typeof(AuthorAttribute), false);
foreach (AuthorAttribute attr in attributes)
{
    Console.WriteLine($"Author: {attr.Name}, Version: {attr.Version}");
}

Dynamic Type Creation & Invocation

// Tạo instance dynamically
Type calculatorType = typeof(Calculator);
object calculator = Activator.CreateInstance(calculatorType);

// Gọi method dynamically
MethodInfo addMethod = calculatorType.GetMethod("Add");
object result = addMethod.Invoke(calculator, new object[] { 5, 3 });
Console.WriteLine($"Result: {result}"); // 8

// Get/Set property values
PropertyInfo nameProperty = calculatorType.GetProperty("Name");
nameProperty.SetValue(calculator, "MyCalculator");
string name = (string)nameProperty.GetValue(calculator);

Performance Considerations

// ❌ Chậm - Reflection mỗi lần
for (int i = 0; i < 1000; i++)
{
    MethodInfo method = obj.GetType().GetMethod("Process");
    method.Invoke(obj, null);
}

// ✅ Tốt hơn - Cache MethodInfo
MethodInfo cachedMethod = obj.GetType().GetMethod("Process");
for (int i = 0; i < 1000; i++)
{
    cachedMethod.Invoke(obj, null);
}

// ✅ Tốt nhất - Delegate (Expression Trees)
var method = obj.GetType().GetMethod("Process");
var delegate = (Action)Delegate.CreateDelegate(typeof(Action), obj, method);
for (int i = 0; i < 1000; i++)
{
    delegate();
}

Use Cases cho Reflection

  1. Dependency Injection Frameworks - Tìm và register services
  2. ORM Frameworks - Map database columns to properties
  3. Serialization/Deserialization - Inspect object structure
  4. Testing Frameworks - Tìm và chạy test methods
  5. Plugin Systems - Load và instantiate plugins dynamically

C# 12 Features

Primary Constructors (C# 12)

public class Point(int X, int Y)
{
    public int Sum() => X + Y;
}

var point = new Point(3, 4);
Console.WriteLine(point.Sum()); // 7

Collection Expressions

// Array
int[] numbers = [1, 2, 3, 4, 5];

// Span
Span<int> span = [1, 2, 3];

// List
List<string> names = ["Alice", "Bob"];

Default Lambda Parameters

Func<int, int, int> add = (int a, int b = 10) => a + b;
Console.WriteLine(add(5)); // 15

Alias Any Type

using IntList = List<int>;
using Point3D = (int x, int y, int z);

CLR & Bộ nhớ

Garbage Collection (GC)

GC hoạt động như thế nào?

Garbage Collection là quản lý bộ nhớ tự động trong .NET, hoạt động theo cơ chế:

// Khi không còn reference, object sẽ được GC thu hồi
public void CreateObject()
{
    var obj = new MyClass(); // Allocate trên Heap
    // Khi method kết thúc, obj không còn reference
    // GC sẽ thu hồi bộ nhớ
}

GC Cycle

  1. Allocation: Khi object mới được tạo, nó được allocate trên Heap
  2. Mark Phase: GC đánh dấu tất cả objects có reference (root objects)
  3. Sweep Phase: Xóa các objects không có reference
  4. Compact Phase: Di chuyển các objects còn lại để giảm fragmentation

Generations

┌─────────────────────────────────────────────────────────────┐
│                        LARGE OBJECT HEAP                    │
│                    (>85KB objects)                          │
├─────────────────────────────────────────────────────────────┤
│  Generation 2 (Long-lived)    │ Gen 1 (Intermediate)       │
│  ┌─────────────────────────┐   │ ┌───────────────────────┐  │
│  │ Static variables        │   │ │ Objects that survived│  │
│  │ Singleton services      │   │ │ Gen 1 collection     │  │
│  └─────────────────────────┘   │ └───────────────────────┘  │
├─────────────────────────────────────────────────────────────┤
│  Generation 0 (Short-lived)                                │
│  ┌───────────────────────────────────────────────────────┐  │
│  │ Local variables, temporary objects                    │  │
│  │ Created and destroyed frequently                      │  │
│  └───────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘
GenerationMô tảTần suất GC
Gen 0Short-lived objects (local variables)Thường xuyên nhất
Gen 1Intermediate objectsÍt hơn Gen 0
Gen 2Long-lived objects (static, singletons)Ít nhất

Tối ưu để giảm áp lực lên GC

1. Sử dụng StringBuilder thay vì String

// ❌ Tạo nhiều string objects
string result = "";
for (int i = 0; i < 1000; i++)
{
    result += i.ToString(); // Mỗi lần tạo new string
}

// ✅ Sử dụng StringBuilder
var sb = new StringBuilder();
for (int i = 0; i < 1000; i++)
{
    sb.Append(i);
}
string result = sb.ToString();

2. Sử dụng struct khi phù hợp

// Value type - lưu trên Stack
public struct Point
{
    public int X { get; set; }
    public int Y { get; set; }
}

// Reference type - lưu trên Heap
public class PointClass
{
    public int X { get; set; }
    public int int Y { get; set; }
}

3. Tránh boxing/unboxing

// ❌ Boxing - chuyển value type sang object
int i = 10;
object o = i; // Boxing

// ✅ Tránh boxing
int i = 10;
long l = i; // Không boxing

4. Sử dụng Span và Memory

// Tránh allocation khi xử lý arrays
Span<int> span = stackalloc int[100]; // Stack allocation
span[0] = 42;

// Hoặc slice without allocation
var slice = span.Slice(0, 10);

5. Dispose objects sử dụng using

// ✅ Tự động gọi Dispose
using var file = new StreamReader("file.txt");
var content = file.ReadToEnd();

// ✅ Hoặc using statement truyền thống
using (var file = new StreamReader("file.txt"))
{
    var content = file.ReadToEnd();
}

6. GC.Collect() - Khi nào nên dùng?

// ❌ Không nên gọi thủ công trong production
GC.Collect();

// ✅ Có thể dùng sau khi giải phóng large objects
public void Cleanup()
{
    largeObject = null;
    GC.Collect();
    GC.WaitForPendingFinalizers();
}

Value Types vs Reference Types

So sánh cách lưu trữ

┌─────────────────────────────────────────────────────────────┐
│                      STACK                │      HEAP       │
├────────────────────────────────────────────┼─────────────────┤
│                                            │                 │
│  int x = 10;           │ 10                 │                 │
│  int y = x;            │ 10  (copy)         │                 │
│                                            │                 │
│  var p1 = new Point(); │ 0x1234 (ref)──────►│ Point { x: 0 }  │
│  var p2 = p1;          │ 0x1234 (ref)──────►│ Point { x: 0 }  │
│                                            │                 │
└────────────────────────────────────────────┴─────────────────┘

Khi nào sử dụng struct?

  • Kích thước nhỏ (<16 bytes)
  • Immutable (không thay đổi sau khi tạo)
  • Không cần inheritance
  • Tạo và hủy thường xuyên (trong loops)
// ✅ Nên dùng struct
public readonly struct Point
{
    public double X { get; }
    public double Y { get; }
    
    public Point(double x, double y) => (X, Y) = (x, y);
}

// ❌ Không nên dùng struct
public struct LargeObject
{
    public byte[] Data { get; set; } // Array - vẫn allocate trên Heap!
}

Performance Impact

OperationValue TypeReference Type
AllocationStack (nhanh)Heap (chậm hơn)
CopyCopy toàn bộCopy reference
PassingCopy toàn bộCopy reference (4/8 bytes)
GC PressureKhông

ref, out, in modifiers

ref - Pass by Reference

public void Swap(ref int a, ref int b)
{
    int temp = a;
    a = b;
    b = temp;
}

int x = 5, y = 10;
Swap(ref x, ref y);
// x = 10, y = 5
  • Caller phải khởi tạo biến trước khi pass
  • Có thể đọc và ghi

out - Output Parameter

public bool TryParse(string input, out int result)
{
    if (int.TryParse(input, out result))
    {
        return true;
    }
    result = 0; // Phải gán trước khi return
    return false;
}

if (TryParse("42", out int number))
{
    Console.WriteLine(number);
}
  • Caller KHÔNG cần khởi tạo
  • Phải gán giá trị trước khi method return
  • Thường dùng cho “output” values

in - Read-only Reference

public void Print(in Point point)
{
    // point.X = 10; // ❌ Lỗi - readonly
    Console.WriteLine(point.X); // ✅ Đọc được
}
  • Không thể modify giá trị
  • Tránh copying khi passing large structs
  • Cải thiện performance cho large structs

So sánh

ModifierCaller initializeCan modifyUse case
ref✅ Yes✅ YesIn-place modification
out❌ No✅ YesReturn multiple values
in✅ Yes❌ NoRead-only, performance

2. ASP.NET Core Cốt lõi

Giới thiệu

Phần này trình bày các kiến thức cốt lõi về ASP.NET Core framework.

Nội dung chính

Khởi tạo ứng dụng

Middleware

Dependency Injection

Routing

Filters

Cấu hình & Logging

Xử lý lỗi

Model Binding

Model Validation

ActionResult Types

Static Files & File Handling

Background Tasks

Health Checks

Minimal APIs Advanced

Content Negotiation

Output Caching

Rate Limiting

Kestrel Configuration

Localization (i18n)

Real-time Communication

gRPC

Khởi tạo Ứng dụng

Program.cs vs Startup.cs

Minimal APIs (.NET 6+)

// Program.cs - Minimal APIs
var builder = WebApplication.CreateBuilder(args);

// Add services
builder.Services.AddDbContext<AppDbContext>();
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure middleware pipeline
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();

Startup.cs (Trước .NET 6)

// Startup.cs - Traditional pattern
public class Startup
{
    public IConfiguration Configuration { get; }
    
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }
    
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDbContext<AppDbContext>();
        services.AddControllers();
    }
    
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        
        app.UseHttpsRedirection();
        app.UseRouting();
        app.UseAuthorization();
        
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
}

// Program.cs
public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }
    
    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

So sánh

AspectMinimal APIsStartup.cs
Code linesÍt hơnNhiều hơn
ComplexityĐơn giảnPhức tạp hơn
FlexibilityHạn chếLinh hoạt
TestabilityKhó test hơnDễ test hơn
Use caseMicroservices, small APIsLarge applications
DI ConfigurationTrong Program.csTrong ConfigureServices

Các phương thức mở rộng thường dùng

Service Registration

// Singleton - Một instance duy nhất cho toàn bộ lifetime của app
builder.Services.AddSingleton<IService, Service>();

// Scoped - Một instance per request
builder.Services.AddScoped<IService, Service>();

// Transient - Instance mới mỗi lần được yêu cầu
builder.Services.AddTransient<IService, Service>();

Middleware Registration

var app = builder.Build();

// Run - Kết thúc pipeline (không gọi next)
app.Run(async context => 
{
    await context.Response.WriteAsync("Hello");
});

// Use - Có thể gọi next middleware
app.Use(async (context, next) =>
{
    // Do something before
    await next();
    // Do something after
});

// Map - Route-based middleware
app.Map("/api", appBuilder =>
{
    appBuilder.UseRouting();
});

Environment-based Configuration

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

if (app.Environment.IsProduction())
{
    app.UseExceptionHandler("/Error");
}

Middleware

Khái niệm

Middleware là các component nằm trong pipeline xử lý request/response. Mỗi middleware có thể:

  • Xử lý request trước khi chuyển cho middleware tiếp theo
  • Xử lý response sau khi các middleware trước đó đã xử lý
  • Quyết định không chuyển request cho middleware tiếp theo (short-circuit)

Pipeline Execution Order

┌────────────────────────────────────────────────────────────────────┐
│                        REQUEST PIPELINE                           │
├────────────────────────────────────────────────────────────────────┤
│                                                                    │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐    │
│  │  Logging │───▶│ Routing  │───▶│ Auth     │───▶│ Endpoint │    │
│  │ Middleware   │ Middleware   │ Middleware   │ (Controller) │    │
│  └──────────┘    └──────────┘    └──────────┘    └──────────┘    │
│       ▲                                                    │       │
│       │                                                    ▼       │
│       │                   ┌──────────┐    ┌──────────┐           │
│       └───────────────────│ Response │◀───│ Error    │           │
│                           │ Middleware   │ Middleware   │           │
│                           └──────────┘    └──────────┘           │
│                                                                    │
└────────────────────────────────────────────────────────────────────┘

Thứ tự quan trọng

  1. Exception Handling - Đầu tiên để bắt exceptions
  2. Security (CORS, Authentication, Authorization)
  3. Static Files - Nếu cần
  4. Routing - Xác định endpoint
  5. Endpoints - Controller/Action
  6. Custom Middleware

Cách viết Custom Middleware

1. Conventional Middleware Class

public class RequestTimingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestTimingMiddleware> _logger;

    public RequestTimingMiddleware(
        RequestDelegate next, 
        ILogger<RequestTimingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var stopwatch = Stopwatch.StartNew();
        
        // Before next middleware
        _logger.LogInformation("Request started: {Path}", context.Request.Path);
        
        await _next(context); // Call next middleware
        
        // After next middleware
        stopwatch.Stop();
        _logger.LogInformation(
            "Request completed in {ElapsedMs}ms", 
            stopwatch.ElapsedMilliseconds);
    }
}

// Extension method để dễ đăng ký
public static class RequestTimingMiddlewareExtensions
{
    public static IApplicationBuilder UseRequestTiming(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<RequestTimingMiddleware>();
    }
}

// Đăng ký trong Program.cs
var app = builder.Build();
app.UseRequestTiming();

2. Inline Middleware (Minimal API)

var app = builder.Build();

app.Use(async (context, next) =>
{
    var stopwatch = Stopwatch.StartNew();
    
    await next();
    
    stopwatch.Stop();
    Console.WriteLine($"Request took {stopwatch.ElapsedMilliseconds}ms");
});

app.MapGet("/", () => "Hello World");
app.Run();

3. Middleware with Dependencies

public class CustomMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IConfiguration _config;

    public CustomMiddleware(
        RequestDelegate next,
        IConfiguration config)
    {
        _next = next;
        _config = config;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // Sử dụng injected dependencies
        var timeout = _config.GetValue<int>("App:Timeout");
        
        await _next(context);
    }
}

Middleware vs Filters

Middleware

  • Hoạt động trên toàn bộ request pipeline
  • Thực thi trước khi routing xác định endpoint
  • Phù hợp cho: Logging, Authentication, CORS, Error handling

Filters (MVC/Web API)

  • Chỉ hoạt động cho MVC/Razor Pages actions
  • Có access đến ActionContext
  • Phù hợp cho: Model validation, Result caching, Exception handling cụ thể

Ví dụ so sánh

// Middleware - Cho toàn bộ app
app.Use(async (context, next) =>
{
    // Log mọi request
    await next();
});

// Filter - Chỉ cho MVC actions
public class ActionLogFilter : IActionFilter
{
    public void OnActionExecuting(ActionExecutingContext context)
    {
        // Log trước khi action chạy
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        // Log sau khi action chạy
    }
}

Dependency Injection

3 Loại Lifecycle

Singleton

// Một instance duy nhất cho toàn bộ application lifetime
builder.Services.AddSingleton<IEmailService, EmailService>();

// Ví dụ sử dụng
public class ProductService
{
    private readonly IEmailService _emailService;
    
    public ProductService(IEmailService emailService)
    {
        _emailService = emailService; // Cùng instance xuyên suốt
    }
}

Khi nào dùng:

  • Configuration services
  • Logger (ILogger)
  • Caching services
  • Services không có state hoặc share state toàn cục

Scoped

// Một instance per HTTP request
builder.Services.AddScoped<IProductRepository, ProductRepository>();

// Ví dụ sử dụng
public class OrderService
{
    private readonly IProductRepository _productRepo;
    
    public OrderService(IProductRepository productRepo)
    {
        _productRepo = productRepo; // Cùng instance trong 1 request
    }
}

Khi nào dùng:

  • DbContext (EF Core)
  • Services cần per-request state
  • Business logic services

Transient

// Instance mới mỗi lần được yêu cầu
builder.Services.AddTransient<IReportGenerator, ReportGenerator>();

// Ví dụ sử dụng
public class DashboardService
{
    private readonly IReportGenerator _reportGenerator;
    
    public DashboardService(IReportGenerator reportGenerator)
    {
        _reportGenerator = reportGenerator; // Instance mới mỗi lần
    }
}

Khi nào dùng:

  • Lightweight, stateless services
  • Services với expensive initialization
  • Khi cần instance riêng biệt cho mỗi lần sử dụng

Ví dụ thực tế

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Singleton - Configuration, Logger
builder.Services.AddSingleton<IConfiguration>(builder.Configuration);
builder.Services.AddSingleton<ILogger<Program>, Logger<Program>>();

// Scoped - DbContext, Repositories
builder.Services.AddScoped<AppDbContext>();
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();

// Transient - Small, stateless services
builder.Services.AddTransient<IEmailSender, EmailSender>();
builder.Services.AddTransient<IDateTimeProvider, DateTimeProvider>();

var app = builder.Build();

Lifecycle Diagram

┌─────────────────────────────────────────────────────────────────┐
│                    APPLICATION LIFETIME                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   ┌─────────────────────────────────────────────────────────┐   │
│   │                    SINGLETON                            │   │
│   │  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐   │   │
│   │  │ Request │  │ Request │  │ Request │  │ Request │   │   │
│   │  │    1    │  │    2    │  │    3    │  │    4    │   │   │
│   │  └─────────┘  └─────────┘  └─────────┘  └─────────┘   │   │
│   │       │            │            │            │        │   │
│   │       └────────────┴────────────┴────────────┘        │   │
│   │       (Cùng một instance cho tất cả requests)         │   │
│   └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│   ┌─────────────────────────────────────────────────────────┐   │
│   │                    SCOPED                               │   │
│   │  ┌─────────┐              ┌─────────┐                  │   │
│   │  │ Request │              │ Request │                  │   │
│   │  │    1    │              │    2    │                  │   │
│   │  │ ┌─────┐ │              │ ┌─────┐ │                  │   │
│   │  │ │ Svc │ │              │ │ Svc │ │                  │   │
│   │  │ └─────┘ │              │ └─────┘ │                  │   │
│   │  └─────────┘              └─────────┘                  │   │
│   │ (Instance mới cho mỗi request, reuse trong request)    │   │
│   └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│   ┌─────────────────────────────────────────────────────────┐   │
│   │                   TRANSIENT                             │   │
│   │  ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐            │   │
│   │  │ S1 │ │ S2 │ │ S3 │ │ S4 │ │ S5 │ │ S6 │            │   │
│   │  └────┘ └────┘ └────┘ └────┘ └────┘ └────┘            │   │
│   │  (Instance mới mỗi lần được request)                   │   │
│   └─────────────────────────────────────────────────────────┘   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Anti-Pattern: Scoped vào Singleton

Tại sao KHÔNG NÊN?

// ❌ BAD - Injecting Scoped service vào Singleton
builder.Services.AddSingleton<BadSingletonService>();

public class BadSingletonService
{
    private readonly AppDbContext _context;
    
    public BadSingletonService(AppDbContext context) // Scoped!
    {
        _context = context; // Sẽ gây lỗi!
    }
}

Vấn đề: Singleton tồn tại xuyên suốt application lifetime, nhưng Scoped service (DbContext) chỉ valid trong một HTTP request. Khi request kết thúc, DbContext bị disposed nhưng Singleton vẫn giữ reference, gây ObjectDisposedException.

Giải pháp

// ✅ GOOD - Inject IServiceProvider vào Singleton
builder.Services.AddSingleton<GoodSingletonService>();

public class GoodSingletonService
{
    private readonly IServiceProvider _serviceProvider;
    
    public GoodSingletonService(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    
    public void DoWork()
    {
        // Tạo scope mới để resolve scoped services
        using var scope = _serviceProvider.CreateScope();
        var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        
        // Sử dụng context trong scope
        var data = context.Products.ToList();
    }
}

Best Practices

  1. Tuân thủ Service Lifetime Hierarchy:

    • ✅ Singleton → Singleton
    • ✅ Singleton → Scoped (thông qua IServiceProvider)
    • ✅ Scoped → Scoped
    • ✅ Scoped → Transient
    • ✅ Transient → Transient
    • ❌ Scoped → Singleton (không có vấn đề về mặt kỹ thuật)
    • ❌ Singleton → Scoped (không nên)
    • ❌ Singleton → Transient (có thể accept)
  2. Register services với interface:

    // ✅ Tốt
    builder.Services.AddScoped<IProductRepository, ProductRepository>();
    
    // ❌ Tránh
    builder.Services.AddScoped<ProductRepository>();
    
  3. Constructor Injection thay vì Property Injection:

    // ✅ Tốt
    public class Service
    {
        private readonly IOtherService _other;
        public Service(IOtherService other) => _other = other;
    }
    
    // ❌ Tránh
    public class Service
    {
        public IOtherService Other { get; set; }
    }
    

Routing

Conventional Routing vs Attribute Routing

Conventional Routing (MVC)

Định nghĩa routes tập trung trong Program.cs hoặc Startup.cs:

// Program.cs
app.UseRouting();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.MapControllerRoute(
    name: "blog",
    pattern: "blog/{*slug}",
    defaults: new { controller = "Blog", action = "Post" });

// Hoặc routes phân cấp
app.MapControllerRoute(
    name: "products",
    pattern: "products/{category}/{action}",
    defaults: new { controller = "Products" });

Controller:

public class ProductsController : Controller
{
    public IActionResult Index()
    {
        return View();
    }
    
    public IActionResult Details(int id)
    {
        return View(id);
    }
}

Attribute Routing (Web API)

Định nghĩa routes trực tiếp trên controller/action:

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet]
    public IActionResult GetAll()
    {
        return Ok();
    }
    
    [HttpGet("{id:int}")]
    public IActionResult GetById(int id)
    {
        return Ok(id);
    }
    
    [HttpPost]
    public IActionResult Create([FromBody] Product product)
    {
        return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
    }
    
    [HttpPut("{id:int}")]
    public IActionResult Update(int id, [FromBody] Product product)
    {
        return NoContent();
    }
    
    [HttpDelete("{id:int}")]
    public IActionResult Delete(int id)
    {
        return NoContent();
    }
}

So sánh

AspectConventional RoutingAttribute Routing
DefinitionTập trung trong configTrực tiếp trên method
FlexibilityLinh hoạt với patternsChi tiết, rõ ràng
RESTfulKhó tạo RESTfulDễ dàng
ConventionTheo quy ước đặt tênTùy chỉnh
Use caseMVC với ViewsWeb API

Route Templates

Basic Templates

[HttpGet("/products")]                    // Exact: /products
[HttpGet("products/{id}")]                 // Parameter: /products/1
[HttpGet("products/{id:int}")]             // Typed: /products/1 (chấp nhận số)
[HttpGet("products/{name:alpha}")]         // Constraint: chỉ chữ cái
[HttpGet("products/{*slug}")]              // Catch-all: /products/a/b/c

Route Constraints

ConstraintExampleDescription
int{id:int}Chỉ số nguyên
bool{active:bool}Chỉ true/false
datetime{date:datetime}Giá trị datetime
guid{id:guid}GUID
length{name:length(3,10)}Độ dài cụ thể
range{age:range(18,100)}Khoảng giá trị
regex{code:regex(^\\d{3}$)}Regex pattern

Optional Parameters

[HttpGet("products/{category?}")]
public IActionResult GetByCategory(string? category)
{
    // category có thể null hoặc có giá trị
}

Default Values

[HttpGet("products")]
public IActionResult GetAll([FromQuery] int page = 1, [FromQuery] int pageSize = 10)
{
    // Default values
}

Route Ordering

Explicit Order với [Route] attribute

[ApiController]
[Route("api/[controller]")]
public class ItemsController : ControllerBase
{
    // Đặt specific routes trước generic routes
    [HttpGet("best-selling")]  // ✅ Matches /api/items/best-selling
    public IActionResult GetBestSelling()
    {
        return Ok();
    }
    
    [HttpGet("{id}")]          // ❌ Sẽ không bao giờ được gọi nếu đặt sau
    public IActionResult GetById(int id)
    {
        return Ok(id);
    }
}

Route Precedence

ASP.NET Core ưu tiên:

  1. Static segments (e.g., api/products)
  2. Route parameters có constraints
  3. Route parameters không constraints
  4. Catch-all parameters

Filters

Các loại Filter

Filters cho phép chạy code tại các điểm cụ thể trong pipeline execution của MVC.

┌─────────────────────────────────────────────────────────────────────┐
│                    MVC REQUEST PIPELINE                             │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  1. Authorization Filter                                             │
│     ↓                                                               │
│  2. Resource Filter (OnExecuting)                                   │
│     ↓                                                               │
│  3. Model Binding                                                   │
│     ↓                                                               │
│  4. Action Filter (OnExecuting)                                     │
│     ↓                                                               │
│  5. Action executes                                                 │
│     ↓                                                               │
│  6. Action Filter (OnExecuted)                                      │
│     ↓                                                               │
│  7. Result Filter (OnExecuting)                                     │
│     ↓                                                               │
│  8. Result executes                                                 │
│     ↓                                                               │
│  9. Result Filter (OnExecuted)                                      │
│     ↓                                                               │
│ 10. Resource Filter (OnExecuted)                                   │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

1. Authorization Filter

public class CustomAuthorizationFilter : IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        var user = context.HttpContext.User;
        
        if (!user.Identity.IsAuthenticated)
        {
            context.Result = new UnauthorizedResult();
        }
    }
}

// Sử dụng
[ServiceFilter(typeof(CustomAuthorizationFilter))]
public class ProductsController : ControllerBase { }

2. Resource Filter

public class TrackRequestFilter : IAsyncResourceFilter
{
    private readonly ILogger<TrackRequestFilter> _logger;

    public TrackRequestFilter(ILogger<TrackRequestFilter> logger)
    {
        _logger = logger;
    }

    public async Task OnResourceExecutingAsync(ResourceExecutingContext context)
    {
        _logger.LogInformation("Resource executing");
        // Thực thi trước khi resource (controller) được gọi
        context.HttpContext.Items["StartTime"] = DateTime.UtcNow;
    }

    public async Task OnResourceExecutedAsync(ResourceExecutedContext context)
    {
        var startTime = (DateTime)context.HttpContext.Items["StartTime"];
        _logger.LogInformation($"Resource executed in {(DateTime.UtcNow - startTime).TotalMilliseconds}ms");
    }
}

3. Action Filter

public class LogActionFilter : IActionFilter
{
    private readonly ILogger<LogActionFilter> _logger;

    public LogActionFilter(ILogger<LogActionFilter> logger)
    {
        _logger = logger;
    }

    public void OnActionExecuting(ActionExecutingContext context)
    {
        _logger.LogInformation("Action executing: {Controller}.{Action}", 
            context.Controller.GetType().Name,
            context.ActionDescriptor.Name);
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        _logger.LogInformation("Action executed");
    }
}

4. Exception Filter

public class GlobalExceptionFilter : IExceptionFilter
{
    private readonly ILogger<GlobalExceptionFilter> _logger;

    public GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger)
    {
        _logger = logger;
    }

    public void OnException(ExceptionContext context)
    {
        _logger.LogError(context.Exception, "Unhandled exception");

        var result = new
        {
            error = "An error occurred",
            message = context.Exception.Message,
            traceId = context.HttpContext.TraceIdentifier
        };

        context.Result = new JsonResult(result)
        {
            StatusCode = 500
        };
        
        context.ExceptionHandled = true;
    }
}

5. Result Filter

public class CacheResultFilter : IResultFilter
{
    public void OnResultExecuting(ResultExecutingContext context)
    {
        if (!context.HttpContext.Response.HasStarted)
        {
            context.HttpContext.Response.Headers["Cache-Control"] = "no-cache";
        }
    }

    public void OnResultExecuted(ResultExecutedContext context)
    {
        // Post-processing
    }
}

Filter Types Comparison

Filter TypeImplementsRunsUse Case
AuthorizationIAuthorizationFilterĐầu tiênCheck authentication/authorization
ResourceIResourceFilterTrước & sau model bindingCaching, performance tracking
ActionIActionFilterTrước & sau actionLogging, validation
ExceptionIExceptionFilterKhi exception xảy raError handling
ResultIResultFilterTrước & sau result executionOutput formatting, caching

Filters vs Middleware

Filters

  • ✅ Chỉ áp dụng cho MVC/Razor Pages
  • ✅ Có access đến ActionContext (model binding results, etc.)
  • ✅ Có thể bind services từ DI
  • ✅ Thực thi sau khi route đã được xác định

Middleware

  • ✅ Áp dụng cho toàn bộ pipeline (bao gồm static files)
  • ✅ Thực thi trước khi routing xác định endpoint
  • ✅ Phù hợp cho cross-cutting concerns không liên quan đến MVC

Khi nào dùng?

// Middleware - Cho toàn bộ app
app.Use(async (context, next) =>
{
    // Log mọi request
    await next();
});

// Filter - Cho MVC actions cụ thể
[ServiceFilter(typeof(MyActionFilter))]
public class ProductsController : ControllerBase { }

Đăng ký Filters

1. Global

builder.Services.AddControllersWithViews()
    .AddMvcOptions(options =>
    {
        options.Filters.Add(new GlobalExceptionFilter());
    });

2. Controller/Action Level

[ControllerLevelFilter]
public class ProductsController : ControllerBase
{
    [ActionLevelFilter]
    public IActionResult Get() { }
}

3. Service Filter

// Đăng ký trong DI
builder.Services.AddScoped<MyFilter>();

// Sử dụng
[ServiceFilter(typeof(MyFilter))]
public IActionResult Get() { }

4. Type Filter

// Không cần đăng ký trong DI
[TypeFilter(typeof(MyFilter))]
public IActionResult Get() { }

Cấu hình & Logging

appsettings.json

Cấu trúc cơ bản

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=MyDb;Trusted_Connection=true;"
  },
  "AppSettings": {
    "MaxItemsPerPage": 50,
    "EnableCache": true
  }
}

Environment-specific Configuration

appsettings.Development.json  // Development
appsettings.Staging.json      // Staging  
appsettings.Production.json   // Production
// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Tự động load appsettings.{Environment}.json
// Development: appsettings.json + appsettings.Development.json

IConfiguration

Truy cập Configuration

public class Startup
{
    public IConfiguration Configuration { get; }
    
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }
    
    public void ConfigureServices(IServiceCollection services)
    {
        // Cách 1: GetValue
        var maxItems = Configuration.GetValue<int>("AppSettings:MaxItemsPerPage", 10);
        
        // Cách 2: GetSection
        var appSettings = Configuration.GetSection("AppSettings").Get<AppSettings>();
        
        // Cách 3: Bind to object
        services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
    }
}

Options Pattern (IOptions)

// Model class
public class AppSettings
{
    public int MaxItemsPerPage { get; set; } = 10;
    public bool EnableCache { get; set; }
    public EmailSettings Email { get; set; }
}

public class EmailSettings
{
    public string SmtpHost { get; set; }
    public int SmtpPort { get; set; }
}

// Đăng ký
builder.Services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));

// Sử dụng
public class ProductService
{
    private readonly AppSettings _settings;
    
    public ProductService(IOptions<AppSettings> options)
    {
        _settings = options.Value;
    }
    
    public int GetPageSize() => _settings.MaxItemsPerPage;
}

IOptions vs IOptionsSnapshot vs IOptionsMonitor

// IOptions - Đọc config một lần khi app khởi động
// Singleton services nên dùng cái này

// IOptionsSnapshot - Đọc lại config mỗi request
// Scoped services nên dùng cái này để có config mới nhất
builder.Services.AddScoped<IOptionsSnapshot<AppSettings>>();

// IOptionsMonitor - Theo dõi thay đổi config real-time
// Dùng cho hot-reload configuration
builder.Services.AddSingleton<IOptionsMonitor<AppSettings>>();

Serilog Logging

Cài đặt

dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File

Cấu hình cơ bản

// Program.cs
using Serilog;

var builder = WebApplication.CreateBuilder(args);

// Configure Serilog
Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Information()
    .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
    .MinimumLevel.Override("System", LogEventLevel.Warning)
    .Enrich.FromLogContext()
    .Enrich.WithProperty("Application", "MyApp")
    .WriteTo.Console(outputTemplate: 
        "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}")
    .WriteTo.File(
        "logs/log-.txt",
        rollingInterval: RollingInterval.Day,
        retainedFileCountLimit: 30,
        outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}")
    .CreateLogger();

builder.Host.UseSerilog();

var app = builder.Build();

// Sử dụng
Log.Information("Application starting");

app.Run();

// Flush log trước khi exit
Log.CloseAndFlush();

Structured Logging

public class ProductService
{
    private readonly ILogger<ProductService> _logger;
    
    public ProductService(ILogger<ProductService> logger)
    {
        _logger = logger;
    }
    
    public void GetProduct(int id)
    {
        // ✅ Structured logging - dễ query và filter
        _logger.LogInformation("Fetching product {ProductId} for user {UserId}", 
            id, 
            _userId);
        
        // ❌ String interpolation - tránh dùng
        _logger.LogInformation($"Fetching product {id}");
    }
}

Log Levels

LevelUsage
VerboseDetailed tracing
DebugDebugging information
InformationGeneral information
WarningSomething unexpected happened
ErrorFunctionality issue
FatalCritical error causing shutdown

Best Practices

1. Sử dụng correct log levels

// ✅ Correct
Log.Debug("Processing request {RequestId}", requestId);
Log.Information("Product {ProductId} created successfully", productId);
Log.Warning("Cache miss for key {CacheKey}", key);
Log.Error(ex, "Failed to process order {OrderId}", orderId);
Log.Fatal(ex, "Application terminating due to unhandled exception");

2. Include context

public async Task<IActionResult> UpdateProduct(int id, [FromBody] Product product)
{
    Log.LogInformation(
        "Updating product {ProductId} by user {UserId}", 
        id, 
        User.Identity.Name);
        
    // ... code
}

3. Tránh logging sensitive data

// ❌ Bad - Log sensitive data
Log.Information("User login: {Email}, Password: {Password}", email, password);

// ✅ Good - Log masked data
Log.Information("User login attempt for {Email}", email);

Xử lý lỗi (Error Handling)

Global Error Handling

UseExceptionHandler

// Program.cs
var app = builder.Build();

// Development - hiển thị chi tiết lỗi
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    // Production - redirect đến error page
    app.UseExceptionHandler("/Error");
    
    // Hoặc custom error handler
    app.UseExceptionHandler(errorApp =>
    {
        errorApp.Run(async context =>
        {
            context.Response.StatusCode = 500;
            context.Response.ContentType = "application/json";
            
            var exception = context.Features.Get<IExceptionHandlerFeature>();
            var error = new
            {
                statusCode = 500,
                message = "An unexpected error occurred",
                traceId = context.TraceIdentifier
            };
            
            await context.Response.WriteAsJsonAsync(error);
        });
    });
}

UseStatusCodePages

// Xử lý các status code 4xx, 5xx
app.UseStatusCodePages();

// Hoặc redirect
app.UseStatusCodePagesWithRedirects("/Error/{0}");

// Hoặc re-execute
app.UseStatusCodePagesWithReExecute("/Error", "?statusCode={0}");

ProblemDetails (RFC 7807)

Cấu trúc ProblemDetails

┌─────────────────────────────────────────────────────────────────┐
│                    PROBLEMDETAILS STRUCTURE                      │
├─────────────────────────────────────────────────────────────────┤
│ {                                                               │
│   "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", │
│   "title": "Bad Request",                                       │
│   "status": 400,                                                │
│   "detail": "The input was invalid",                           │
│   "instance": "/api/products",                                  │
│   "traceId": "00-abc123-xyz789-00",                            │
│   "errors": {                                                   │
│     "Name": ["The Name field is required"],                     │
│     "Price": ["Price must be greater than 0"]                   │
│   }                                                             │
│ }                                                               │
└─────────────────────────────────────────────────────────────────┘

Cấu hình ProblemDetails

// Program.cs
builder.Services.AddProblemDetails(options =>
{
    options.CustomizeProblemDetails = context =>
    {
        context.ProblemDetails.Extensions["traceId"] = 
            context.HttpContext.TraceIdentifier;
        context.ProblemDetails.Extensions["requestId"] = 
            context.HttpContext.Request.Headers["X-Request-Id"].FirstOrDefault();
    };
});

// Sử dụng trong controller
[HttpPost]
public IActionResult CreateProduct(Product product)
{
    if (product.Price <= 0)
    {
        return Problem(
            title: "Invalid Price",
            detail: "Price must be greater than 0",
            statusCode: 400,
            type: "https://tools.ietf.org/html/rfc7231#section-6.5.1",
            instance: "/api/products");
    }
    
    return Ok(product);
}

Custom Exception Filter

Global Exception Handler

public class GlobalExceptionHandler : IExceptionHandler
{
    private readonly ILogger<GlobalExceptionHandler> _logger;

    public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
    {
        _logger = logger;
    }

    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        _logger.LogError(exception, "Unhandled exception occurred");

        var problemDetails = new ProblemDetails
        {
            Status = StatusCodes.Status500InternalServerError,
            Title = "Server Error",
            Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.6.1",
            Instance = httpContext.Request.Path
        };

        httpContext.Response.StatusCode = problemDetails.Status.Value;
        await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);

        return true; // Exception handled
    }
}

// Đăng ký
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();

Exception Type Handler

public class NotFoundException : Exception
{
    public NotFoundException(string message) : base(message) { }
}

public class ValidationException : Exception
{
    public IDictionary<string, string[]> Errors { get; }

    public ValidationException(IDictionary<string, string[]> errors)
        : base("Validation failed")
    {
        Errors = errors;
    }
}

// Handler cho từng loại exception
public class NotFoundExceptionHandler : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        if (exception is not NotFoundException notFoundEx)
        {
            return false; // Không handle, để handler khác xử lý
        }

        var problemDetails = new ProblemDetails
        {
            Status = StatusCodes.Status404NotFound,
            Title = "Not Found",
            Detail = notFoundEx.Message,
            Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.4"
        };

        httpContext.Response.StatusCode = 404;
        await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
        return true;
    }
}

// Đăng ký theo thứ tự ưu tiên
builder.Services.AddExceptionHandler<NotFoundExceptionHandler>();
builder.Services.AddExceptionHandler<ValidationExceptionHandler>();
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();

Error Handling trong Minimal APIs

var app = builder.Build();

// Global error handler
app.UseExceptionHandler(exceptionHandlerApp =>
{
    exceptionHandlerApp.Run(async context =>
    {
        var exception = context.Features.Get<IExceptionHandlerFeature>();
        var problemDetails = new ProblemDetails
        {
            Status = 500,
            Title = "Internal Server Error",
            Instance = context.Request.Path
        };

        context.Response.StatusCode = 500;
        await context.Response.WriteAsJsonAsync(problemDetails);
    });
});

// Typed Results với error handling
app.MapPost("/api/products", async (Product product, AppDbContext db) =>
{
    try
    {
        db.Products.Add(product);
        await db.SaveChangesAsync();
        return Results.Created($"/api/products/{product.Id}", product);
    }
    catch (DbUpdateException ex)
    {
        return Results.Problem(
            title: "Database Error",
            detail: ex.Message,
            statusCode: 500);
    }
});

Best Practices

1. Không expose sensitive information

// ❌ Bad - expose internal details
return Problem(
    detail: $"SQL Error: {ex.Message} Connection: {connectionString}",
    statusCode: 500);

// ✅ Good - generic message
return Problem(
    detail: "An unexpected error occurred. Please try again later.",
    statusCode: 500);

2. Log errors properly

// ✅ Log với context
_logger.LogError(ex, 
    "Failed to process order {OrderId} for user {UserId}", 
    orderId, userId);

// ✅ Include request details
_logger.LogWarning(
    "Rate limit exceeded for IP {IP} on endpoint {Endpoint}",
    context.Connection.RemoteIpAddress,
    context.Request.Path);

3. Use appropriate status codes

Status CodeWhen to Use
400 Bad RequestInvalid input, validation failed
401 UnauthorizedMissing or invalid authentication
403 ForbiddenAuthenticated but no permission
404 Not FoundResource doesn’t exist
409 ConflictBusiness rule violation, duplicate
422 UnprocessableValid syntax but semantic errors
429 Too Many RequestsRate limit exceeded
500 Internal Server ErrorUnexpected server error
503 Service UnavailableMaintenance or overload

Model Binding

Khái niệm

Model Binding là quá trình ASP.NET Core chuyển đổi dữ liệu từ HTTP request thành các tham số của action method.

┌─────────────────────────────────────────────────────────────────┐
│                    MODEL BINDING SOURCES                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐       │
│  │   Form   │  │   Route  │  │  Query   │  │   Body   │       │
│  │  Values  │  │  Values  │  │  String  │  │  (JSON)  │       │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘  └────┬─────┘       │
│       │             │             │             │              │
│       └─────────────┴─────────────┴─────────────┘              │
│                         │                                      │
│                         ▼                                      │
│              ┌─────────────────────┐                           │
│              │   Model Binder      │                           │
│              │  (Type Conversion)  │                           │
│              └─────────┬───────────┘                           │
│                        │                                       │
│                        ▼                                       │
│              ┌─────────────────────┐                           │
│              │   Action Method     │                           │
│              │   Parameters        │                           │
│              └─────────────────────┘                           │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Binding Sources

[FromBody]

// JSON request body
[HttpPost]
public IActionResult Create([FromBody] Product product)
{
    // product được deserialize từ JSON body
    return Ok(product);
}

// Request:
// POST /api/products
// Content-Type: application/json
// {"name": "Laptop", "price": 999.99}

[FromQuery]

// Query string parameters
[HttpGet]
public IActionResult GetProducts(
    [FromQuery] int page = 1,
    [FromQuery] int pageSize = 10,
    [FromQuery] string? search = null)
{
    // /api/products?page=2&pageSize=20&search=laptop
    return Ok(new { page, pageSize, search });
}

// Hoặc binding vào object
[HttpGet]
public IActionResult GetProducts([FromQuery] PagingRequest request)
{
    return Ok(request);
}

public class PagingRequest
{
    public int Page { get; set; } = 1;
    public int PageSize { get; set; } = 10;
    public string? Search { get; set; }
}

[FromRoute]

// Route data
[HttpGet("{id}")]
public IActionResult GetById([FromRoute] int id)
{
    // /api/products/123 -> id = 123
    return Ok(id);
}

// Multiple route parameters
[HttpGet("{category}/{id}")]
public IActionResult GetByCategoryAndId(
    [FromRoute] string category,
    [FromRoute] int id)
{
    // /api/products/electronics/123
    return Ok(new { category, id });
}

[FromHeader]

[HttpGet]
public IActionResult GetWithHeader([FromHeader] string apiKey)
{
    // Lấy từ header: X-API-Key: abc123
    return Ok(apiKey);
}

// Hoặc lấy tất cả headers
[HttpGet]
public IActionResult GetHeaders([FromHeader] IHeaderDictionary headers)
{
    return Ok(headers);
}

[FromForm]

// Form data (multipart/form-data)
[HttpPost]
public IActionResult CreateForm([FromForm] ProductForm form)
{
    return Ok(form);
}

public class ProductForm
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    public IFormFile Image { get; set; }
}

Binding Order

ASP.NET Core tìm kiếm giá trị theo thứ tự:

  1. Form values - POST form data
  2. Route values - URL route parameters
  3. Query string - URL query parameters
  4. Request body - JSON/XML (cho complex types)
// Không cần attribute cho complex types (mặc định là [FromBody])
[HttpPost]
public IActionResult Create(Product product) // Tự động [FromBody]
{
    return Ok(product);
}

// Cần attribute cho simple types
[HttpGet]
public IActionResult Get(int id) // Tự động [FromQuery]
{
    return Ok(id);
}

Custom Model Binding

Custom Model Binder

public class CommaSeparatedListBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).FirstValue;
        
        if (string.IsNullOrEmpty(value))
        {
            bindingContext.Result = ModelBindingResult.Success(new List<string>());
            return Task.CompletedTask;
        }

        var list = value.Split(',', StringSplitOptions.RemoveEmptyEntries)
                       .Select(x => x.Trim())
                       .ToList();
        
        bindingContext.Result = ModelBindingResult.Success(list);
        return Task.CompletedTask;
    }
}

// Sử dụng
[HttpGet]
public IActionResult GetTags([ModelBinder(typeof(CommaSeparatedListBinder))] List<string> tags)
{
    // /api/products?tags=csharp,dotnet,web -> ["csharp", "dotnet", "web"]
    return Ok(tags);
}

Model Binder Provider

public class CommaSeparatedListBinderProvider : IModelBinderProvider
{
    public IModelBinder? GetBinder(ModelBinderProviderContext context)
    {
        if (context.Metadata.ModelType == typeof(List<string>))
        {
            return new BinderTypeModelBinder(typeof(CommaSeparatedListBinder));
        }
        return null;
    }
}

// Đăng ký
builder.Services.AddControllers(options =>
{
    options.ModelBinderProviders.Insert(0, new CommaSeparatedListBinderProvider());
});

Binding Complex Types

Nested Objects

public class Address
{
    public string Street { get; set; }
    public string City { get; set; }
    public string Country { get; set; }
}

public class Customer
{
    public string Name { get; set; }
    public Address Address { get; set; }
    public List<string> Tags { get; set; }
}

// JSON binding
// {
//   "name": "John",
//   "address": {
//     "street": "123 Main St",
//     "city": "NYC",
//     "country": "USA"
//   },
//   "tags": ["vip", "active"]
// }

Collections

[HttpPost]
public IActionResult CreateMany([FromBody] List<Product> products)
{
    // [
    //   {"name": "Product 1", "price": 100},
    //   {"name": "Product 2", "price": 200}
    // ]
    return Ok(products.Count);
}

Binding với Minimal APIs

// Tự động binding
app.MapPost("/api/products", (Product product) => Results.Ok(product));

// Binding từ query string
app.MapGet("/api/products", (int page, int pageSize) => 
    Results.Ok(new { page, pageSize }));

// Binding từ route
app.MapGet("/api/products/{id}", (int id) => Results.Ok(id));

// Custom binding
app.MapGet("/api/products", ([FromQuery] PagingRequest request) => 
    Results.Ok(request));

// Binding từ header
app.MapGet("/api/secure", ([FromHeader(Name = "X-API-Key")] string apiKey) =>
{
    if (apiKey != "secret") return Results.Unauthorized();
    return Results.Ok("Authorized");
});

Best Practices

1. Sử dụng đúng binding source

// ✅ Rõ ràng
public IActionResult Get(
    [FromRoute] int id,
    [FromQuery] string? search,
    [FromBody] Product product)

// ❌ Không rõ ràng
public IActionResult Get(int id, string search, Product product)

2. Sử dụng nullable cho optional parameters

// ✅ Good
public IActionResult Get([FromQuery] string? search = null)

// ❌ Bad - sẽ throw nếu không có query param
public IActionResult Get([FromQuery] string search)

3. Validate binding

[HttpPost]
public IActionResult Create([FromBody] Product product)
{
    if (product == null)
    {
        return BadRequest("Invalid request body");
    }
    // ...
}

Model Validation

Overview Questions

  • Làm thế nào để đảm bảo dữ liệu nhận từ client là hợp lệ?
  • Data Annotations là gì và sử dụng như thế nào?
  • FluentValidation khác Data Annotations ra sao?
  • Làm sao để tạo custom validation attribute?
  • Validation response format chuẩn là gì?
  • [ApiController] tự động validation như thế nào?

Data Annotations Validation

Built-in Validation Attributes

public class Product
{
    public int Id { get; set; }
    
    [Required(ErrorMessage = "Name is required")]
    [StringLength(100, MinimumLength = 2, ErrorMessage = "Name must be 2-100 characters")]
    public string Name { get; set; } = string.Empty;
    
    [Range(0.01, 9999.99, ErrorMessage = "Price must be between 0.01 and 9999.99")]
    public decimal Price { get; set; }
    
    [EmailAddress(ErrorMessage = "Invalid email format")]
    public string? Email { get; set; }
    
    [Url(ErrorMessage = "Invalid URL format")]
    public string? Website { get; set; }
    
    [Phone(ErrorMessage = "Invalid phone number")]
    public string? Phone { get; set; }
    
    [RegularExpression(@"^[A-Z]{3}$", ErrorMessage = "Code must be 3 uppercase letters")]
    public string? Code { get; set; }
    
    [Compare("Password", ErrorMessage = "Passwords do not match")]
    public string? ConfirmPassword { get; set; }
}

Validation trong Controller

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpPost]
    public IActionResult Create(Product product)
    {
        // [ApiController] tự động check ModelState.IsValid
        // Nếu invalid, tự động return 400 Bad Request
        
        // Xử lý khi valid
        return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
    }
    
    [HttpGet("{id}")]
    public IActionResult GetById(int id)
    {
        return Ok(new { id });
    }
}

Validation Error Response

{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "Name": ["Name is required"],
    "Price": ["Price must be between 0.01 and 9999.99"]
  }
}

Custom Validation Attribute

Simple Custom Attribute

public class MinimumAgeAttribute : ValidationAttribute
{
    private readonly int _minimumAge;

    public MinimumAgeAttribute(int minimumAge)
    {
        _minimumAge = minimumAge;
    }

    protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
    {
        if (value is DateTime birthDate)
        {
            var age = DateTime.Today.Year - birthDate.Year;
            if (birthDate > DateTime.Today.AddYears(-age)) age--;
            
            if (age < _minimumAge)
            {
                return new ValidationResult($"Must be at least {_minimumAge} years old");
            }
        }
        
        return ValidationResult.Success;
    }
}

// Sử dụng
public class UserRegistration
{
    [Required]
    public string Name { get; set; } = string.Empty;
    
    [MinimumAge(18, ErrorMessage = "You must be at least 18 years old")]
    public DateTime DateOfBirth { get; set; }
}

Property Comparison Validation

public class DateRangeAttribute : ValidationAttribute
{
    private readonly string _startDateProperty;
    private readonly string _endDateProperty;

    public DateRangeAttribute(string startDateProperty, string endDateProperty)
    {
        _startDateProperty = startDateProperty;
        _endDateProperty = endDateProperty;
    }

    protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
    {
        var startDateProperty = validationContext.ObjectType.GetProperty(_startDateProperty);
        var endDateProperty = validationContext.ObjectType.GetProperty(_endDateProperty);

        if (startDateProperty?.GetValue(validationContext.ObjectInstance) is DateTime start &&
            endDateProperty?.GetValue(validationContext.ObjectInstance) is DateTime end)
        {
            if (start >= end)
            {
                return new ValidationResult("Start date must be before end date");
            }
        }

        return ValidationResult.Success;
    }
}

// Sử dụng
[DateRange("StartDate", "EndDate", ErrorMessage = "Invalid date range")]
public class Event
{
    public DateTime StartDate { get; set; }
    public DateTime EndDate { get; set; }
}

IValidatableObject

Self-Validating Model

public class Order : IValidatableObject
{
    public int Id { get; set; }
    public DateTime OrderDate { get; set; }
    public DateTime? ShipDate { get; set; }
    public List<OrderItem> Items { get; set; } = new();
    public string? CouponCode { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        // Cross-property validation
        if (ShipDate.HasValue && ShipDate <= OrderDate)
        {
            yield return new ValidationResult(
                "Ship date must be after order date",
                new[] { nameof(ShipDate) });
        }

        // Collection validation
        if (!Items.Any())
        {
            yield return new ValidationResult(
                "Order must have at least one item",
                new[] { nameof(Items) });
        }

        // Business rule validation
        if (Items.Count > 10 && string.IsNullOrEmpty(CouponCode))
        {
            yield return new ValidationResult(
                "Orders with more than 10 items require a coupon code",
                new[] { nameof(CouponCode) });
        }
    }
}

public class OrderItem
{
    public int ProductId { get; set; }
    public int Quantity { get; set; }
    public decimal Price { get; set; }
}

FluentValidation

Cài đặt

dotnet add package FluentValidation.AspNetCore

Cấu hình

// Program.cs
builder.Services.AddControllers()
    .AddFluentValidation(fv =>
    {
        fv.RegisterValidatorsFromAssemblyContaining<Program>();
        fv.DisableDataAnnotationsValidation = true; // Optional: disable data annotations
    });

Tạo Validator

public class ProductValidator : AbstractValidator<Product>
{
    public ProductValidator()
    {
        RuleFor(x => x.Name)
            .NotEmpty().WithMessage("Name is required")
            .MaximumLength(100).WithMessage("Name cannot exceed 100 characters");
            
        RuleFor(x => x.Price)
            .GreaterThan(0).WithMessage("Price must be greater than 0")
            .LessThan(10000).WithMessage("Price cannot exceed 10,000");
            
        RuleFor(x => x.Email)
            .EmailAddress().WithMessage("Invalid email format")
            .When(x => !string.IsNullOrEmpty(x.Email));
            
        RuleFor(x => x.Category)
            .Must(BeAValidCategory).WithMessage("Invalid category")
            .When(x => !string.IsNullOrEmpty(x.Category));
            
        // Conditional validation
        RuleFor(x => x.DiscountCode)
            .NotEmpty().WithMessage("Discount code is required for orders over $100")
            .When(x => x.Price > 100);
    }
    
    private bool BeAValidCategory(string category)
    {
        var validCategories = new[] { "Electronics", "Books", "Clothing", "Food" };
        return validCategories.Contains(category);
    }
}

Custom Validators với FluentValidation

public static class CustomValidators
{
    public static IRuleBuilderOptions<T, string> MustBeValidPhoneNumber<T>(
        this IRuleBuilder<T, string> ruleBuilder)
    {
        return ruleBuilder.Must(phone => 
            phone != null && phone.All(char.IsDigit) && phone.Length >= 10 && phone.Length <= 15)
            .WithMessage("Phone number must be 10-15 digits");
    }
    
    public static IRuleBuilderOptions<T, string> MustBeStrongPassword<T>(
        this IRuleBuilder<T, string> ruleBuilder)
    {
        return ruleBuilder
            .MinimumLength(8).WithMessage("Password must be at least 8 characters")
            .Matches("[A-Z]").WithMessage("Password must contain uppercase letter")
            .Matches("[a-z]").WithMessage("Password must contain lowercase letter")
            .Matches("[0-9]").WithMessage("Password must contain digit")
            .Matches("[^a-zA-Z0-9]").WithMessage("Password must contain special character");
    }
}

// Sử dụng
public class UserRegistrationValidator : AbstractValidator<UserRegistration>
{
    public UserRegistrationValidator()
    {
        RuleFor(x => x.Phone).MustBeValidPhoneNumber();
        RuleFor(x => x.Password).MustBeStrongPassword();
    }
}

Validation Pipeline

Custom Validation Filter

public class ValidateModelAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        if (!context.ModelState.IsValid)
        {
            var errors = context.ModelState
                .Where(e => e.Value?.Errors.Count > 0)
                .ToDictionary(
                    kvp => kvp.Key,
                    kvp => kvp.Value?.Errors.Select(e => e.ErrorMessage).ToArray()
                );

            var response = new
            {
                type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
                title = "Validation Error",
                status = 400,
                errors = errors
            };

            context.Result = new BadRequestObjectResult(response);
        }
    }
}

// Đăng ký global
builder.Services.AddControllers(options =>
{
    options.Filters.Add<ValidateModelAttribute>();
});

Validation trong Minimal APIs

app.MapPost("/api/products", async (Product product, AppDbContext db) =>
{
    var validationContext = new ValidationContext<Product>(product);
    var validator = new ProductValidator();
    var validationResult = await validator.ValidateAsync(validationContext);
    
    if (!validationResult.IsValid)
    {
        var errors = validationResult.Errors
            .GroupBy(e => e.PropertyName)
            .ToDictionary(
                g => g.Key,
                g => g.Select(e => e.ErrorMessage).ToArray()
            );
        
        return Results.ValidationProblem(errors);
    }
    
    db.Products.Add(product);
    await db.SaveChangesAsync();
    return Results.Created($"/api/products/{product.Id}", product);
});

Best Practices

1. Kết hợp Data Annotations và FluentValidation

// Data Annotations cho simple validation
public class Product
{
    [Required]
    public string Name { get; set; } = string.Empty;
    
    [Range(0.01, 9999.99)]
    public decimal Price { get; set; }
}

// FluentValidation cho complex validation
public class ProductValidator : AbstractValidator<Product>
{
    public ProductValidator()
    {
        RuleFor(x => x.Name)
            .MustAsync(BeUniqueName).WithMessage("Product name must be unique");
            
        RuleFor(x => x.Price)
            .MustAsync(NotExceedBudget).WithMessage("Price exceeds budget limit");
    }
}

2. Validation ở nhiều layers

┌─────────────────────────────────────────────────────────────────┐
│                    VALIDATION LAYERS                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Layer 1: API Level (Data Annotations / FluentValidation)       │
│  - Input format validation                                      │
│  - Required fields                                              │
│  - Range/length constraints                                     │
│                                                                 │
│  Layer 2: Service Level (Business Rules)                        │
│  - Business logic validation                                    │
│  - Cross-entity validation                                      │
│  - Database-dependent validation                                │
│                                                                 │
│  Layer 3: Domain Level (Invariants)                             │
│  - Domain invariants                                            │
│  - Entity consistency                                           │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

3. Fail Fast

// ✅ Validate sớm
public async Task<IActionResult> Create(Product product)
{
    // Validation xảy ra trước khi xử lý business logic
    if (!ModelState.IsValid)
        return BadRequest(ModelState);
    
    // Chỉ chạy khi valid
    await _service.ProcessAsync(product);
    return Ok();
}

ActionResult Types

Overview Questions

  • IActionResultActionResult<T> khác nhau như thế nào?
  • Khi nào nên dùng IActionResult vs ActionResult<T>?
  • Các helper methods nào có sẵn trong ControllerBase?
  • TypedResults trong Minimal APIs là gì?
  • Làm sao để return custom response format?

IActionResult

Interface cơ bản

// IActionResult là interface base cho tất cả action results
public interface IActionResult
{
    Task ExecuteResultAsync(ActionContext context);
}

Common Action Results

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    // 200 OK
    [HttpGet]
    public IActionResult GetAll()
    {
        return Ok(products);
        // Equivalent: return new OkObjectResult(products);
    }
    
    // 200 OK với object
    [HttpGet("{id}")]
    public IActionResult GetById(int id)
    {
        return Ok(new { id, name = "Product" });
    }
    
    // 201 Created
    [HttpPost]
    public IActionResult Create(Product product)
    {
        var createdProduct = _service.Create(product);
        return CreatedAtAction(
            nameof(GetById), 
            new { id = createdProduct.Id }, 
            createdProduct);
    }
    
    // 204 No Content
    [HttpDelete("{id}")]
    public IActionResult Delete(int id)
    {
        _service.Delete(id);
        return NoContent();
    }
    
    // 400 Bad Request
    [HttpPost("validate")]
    public IActionResult Validate(Product product)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }
        return Ok();
    }
    
    // 404 Not Found
    [HttpGet("{id}")]
    public IActionResult Get(int id)
    {
        var product = _service.GetById(id);
        if (product == null)
        {
            return NotFound();
        }
        return Ok(product);
    }
    
    // 401 Unauthorized
    [HttpGet("secret")]
    public IActionResult GetSecret()
    {
        return Unauthorized();
    }
    
    // 403 Forbidden
    [HttpGet("admin")]
    public IActionResult GetAdmin()
    {
        return Forbid();
    }
    
    // 409 Conflict
    [HttpPost("check-name")]
    public IActionResult CheckName(string name)
    {
        if (_service.NameExists(name))
        {
            return Conflict(new { error = "Name already exists" });
        }
        return Ok();
    }
    
    // 422 Unprocessable Entity
    [HttpPost("process")]
    public IActionResult Process(Order order)
    {
        if (!CanProcess(order))
        {
            return UnprocessableEntity(new { error = "Cannot process order" });
        }
        return Ok();
    }
    
    // 500 Internal Server Error
    [HttpGet("error")]
    public IActionResult TriggerError()
    {
        return StatusCode(500, new { error = "Internal server error" });
    }
}

ActionResult

Generic ActionResult

// ActionResult<T> cho phép return cả IActionResult và T
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    // Return T - tự động wrap thành OkObjectResult
    [HttpGet]
    public ActionResult<List<Product>> GetAll()
    {
        return _service.GetAll();
    }
    
    // Return IActionResult khi cần status code khác
    [HttpGet("{id}")]
    public ActionResult<Product> GetById(int id)
    {
        var product = _service.GetById(id);
        if (product == null)
        {
            return NotFound(); // IActionResult
        }
        return product; // T
    }
    
    // Mixed returns
    [HttpPost]
    public ActionResult<Product> Create(Product product)
    {
        var created = _service.Create(product);
        return CreatedAtAction(
            nameof(GetById), 
            new { id = created.Id }, 
            created);
    }
}

So sánh IActionResult vs ActionResult

AspectIActionResultActionResult
Return typeChỉ IActionResultCả IActionResult và T
Swagger/OpenAPIKhông rõ response typeTự động generate schema
Type safetyKhông
Use caseKhi cần linh hoạtKhi có response type rõ ràng

Content Results

Different Content Types

[HttpGet("json")]
public IActionResult GetJson()
{
    return Json(new { message = "Hello" });
}

[HttpGet("text")]
public IActionResult GetText()
{
    return Content("Hello World", "text/plain");
}

[HttpGet("html")]
public IActionResult GetHtml()
{
    return Content("<h1>Hello</h1>", "text/html");
}

[HttpGet("xml")]
public IActionResult GetXml()
{
    return Content("<message>Hello</message>", "application/xml");
}

File Results

File Downloads

[HttpGet("download")]
public IActionResult DownloadFile()
{
    var bytes = File.ReadAllBytes("path/to/file.pdf");
    return File(bytes, "application/pdf", "document.pdf");
}

[HttpGet("download-stream")]
public IActionResult DownloadStream()
{
    var stream = new FileStream("path/to/file.pdf", FileMode.Open);
    return File(stream, "application/pdf", "document.pdf");
}

[HttpGet("download-virtual")]
public IActionResult DownloadVirtualFile()
{
    return PhysicalFile(
        @"C:\files\document.pdf", 
        "application/pdf", 
        "document.pdf");
}

Redirect Results

Redirects

[HttpGet("redirect")]
public IActionResult RedirectExample()
{
    // 302 Found
    return Redirect("https://example.com");
    
    // 301 Moved Permanently
    return RedirectPermanent("https://example.com");
    
    // Redirect to action
    return RedirectToAction("GetAll");
    
    // Redirect to route
    return RedirectToRoute("default", new { controller = "Home", action = "Index" });
}

TypedResults (Minimal APIs)

.NET 7+ TypedResults

var app = builder.Build();

// TypedResults cung cấp type-safe results
app.MapGet("/api/products/{id}", async (int id, AppDbContext db) =>
{
    var product = await db.Products.FindAsync(id);
    if (product == null)
    {
        return TypedResults.NotFound();
    }
    return TypedResults.Ok(product);
});

app.MapPost("/api/products", async (Product product, AppDbContext db) =>
{
    db.Products.Add(product);
    await db.SaveChangesAsync();
    return TypedResults.Created($"/api/products/{product.Id}", product);
});

app.MapDelete("/api/products/{id}", async (int id, AppDbContext db) =>
{
    var product = await db.Products.FindAsync(id);
    if (product == null)
    {
        return TypedResults.NotFound();
    }
    db.Products.Remove(product);
    await db.SaveChangesAsync();
    return TypedResults.NoContent();
});

TypedResults vs Results

// Results - trả về IActionResult
app.MapGet("/old", () => Results.Ok("Hello"));

// TypedResults - trả về specific type (better for OpenAPI)
app.MapGet("/new", () => TypedResults.Ok("Hello"));

// TypedResults tốt hơn cho OpenAPI generation
app.MapGet("/api/products", () => TypedResults.Ok(new List<Product>()))
    .Produces<List<Product>>(200)
    .ProducesProblem(500);

Custom ActionResult

Custom Result Class

public class PagedResult<T> : IActionResult
{
    private readonly IEnumerable<T> _items;
    private readonly int _total;
    private readonly int _page;
    private readonly int _pageSize;

    public PagedResult(IEnumerable<T> items, int total, int page, int pageSize)
    {
        _items = items;
        _total = total;
        _page = page;
        _pageSize = pageSize;
    }

    public async Task ExecuteResultAsync(ActionContext context)
    {
        var response = context.HttpContext.Response;
        response.ContentType = "application/json";
        
        var result = new
        {
            items = _items,
            total = _total,
            page = _page,
            pageSize = _pageSize,
            totalPages = (int)Math.Ceiling((double)_total / _pageSize)
        };
        
        await JsonSerializer.SerializeAsync(response.Body, result);
    }
}

// Sử dụng
[HttpGet]
public IActionResult GetProducts([FromQuery] int page = 1, [FromQuery] int pageSize = 10)
{
    var (items, total) = _service.GetPaged(page, pageSize);
    return new PagedResult<Product>(items, total, page, pageSize);
}

Best Practices

1. Sử dụng ActionResult cho API rõ ràng

// ✅ Tốt - rõ ràng response type
[HttpGet]
public ActionResult<List<Product>> GetAll() => _service.GetAll();

// ❌ Tránh - không rõ response type
[HttpGet]
public IActionResult GetAll() => Ok(_service.GetAll());

2. Sử dụng helper methods

// ✅ Tốt
return Ok(data);
return NotFound();
return BadRequest(ModelState);

// ❌ Dài dòng
return new ObjectResult(data) { StatusCode = 200 };
return new ObjectResult(null) { StatusCode = 404 };

3. Consistent error responses

// ✅ Sử dụng ProblemDetails
return Problem(
    title: "Not Found",
    detail: $"Product with id {id} not found",
    statusCode: 404);

// ❌ Inconsistent
return NotFound(new { error = "Not found", message = "Product not found" });

Static Files

Overview Questions

  • ASP.NET Core phục vụ static files như thế nào?
  • wwwroot folder là gì và cấu hình ra sao?
  • Làm sao để enable directory browsing?
  • File providers là gì và khi nào cần custom file provider?
  • Cache headers cho static files được cấu hình ra sao?

Serving Static Files

Default Configuration

// Program.cs
var app = builder.Build();

// Enable static files middleware
app.UseStaticFiles();

// Static files được phục vụ từ wwwroot folder
// http://localhost/images/logo.png -> wwwroot/images/logo.png

Custom Static File Location

// Serve từ folder khác
app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new PhysicalFileProvider(
        Path.Combine(builder.Environment.ContentRootPath, "MyStaticFiles")),
    RequestPath = "/static"
});

// http://localhost/static/logo.png -> MyStaticFiles/logo.png

wwwroot Structure

Project/
├── wwwroot/
│   ├── css/
│   │   └── site.css
│   ├── js/
│   │   └── site.js
│   ├── images/
│   │   └── logo.png
│   └── lib/
│       └── bootstrap/
├── Program.cs
└── appsettings.json

HTML Reference

<!DOCTYPE html>
<html>
<head>
    <link rel="stylesheet" href="/css/site.css" />
</head>
<body>
    <img src="/images/logo.png" alt="Logo" />
    <script src="/js/site.js"></script>
</body>
</html>

Directory Browsing

Enable Directory Browsing

var app = builder.Build();

// Enable directory browsing
app.UseDirectoryBrowser(new DirectoryBrowserOptions
{
    FileProvider = new PhysicalFileProvider(
        Path.Combine(builder.Environment.ContentRootPath, "wwwroot")),
    RequestPath = "/files"
});

// Hoặc cho toàn bộ static files
app.UseStaticFiles();
app.UseDirectoryBrowser();

Security Warning

⚠️ Chỉ enable directory browsing cho development hoặc khi cần thiết. Production nên disable để tránh expose file structure.


File Providers

Physical File Provider

// Default - sử dụng PhysicalFileProvider
app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new PhysicalFileProvider(
        Path.Combine(builder.Environment.ContentRootPath, "wwwroot"))
});

Embedded File Provider

// Serve files từ embedded resources
app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new EmbeddedFileProvider(
        typeof(Program).Assembly,
        "MyApp.EmbeddedFiles")
});

Composite File Provider

// Kết hợp nhiều file providers
var compositeProvider = new CompositeFileProvider(
    new PhysicalFileProvider(Path.Combine(builder.Environment.ContentRootPath, "wwwroot")),
    new EmbeddedFileProvider(typeof(Program).Assembly, "MyApp.EmbeddedFiles")
);

app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = compositeProvider
});

Content Types

Default Content Types

// ASP.NET Core tự động detect content type từ file extension
// .css -> text/css
// .js  -> application/javascript
// .png -> image/png
// .html -> text/html

Custom Content Types

var provider = new FileExtensionContentTypeProvider();

// Add custom mapping
provider.Mappings[".webp"] = "image/webp";
provider.Mappings[".custom"] = "application/x-custom";

// Remove mapping
provider.Mappings.Remove(".rtf");

app.UseStaticFiles(new StaticFileOptions
{
    ContentTypeProvider = provider
});

Cache Headers

Default Behavior

// Static files middleware tự động thêm Last-Modified header
// Browser sẽ cache và send If-Modified-Since request
// Server trả về 304 Not Modified nếu file không thay đổi

Custom Cache Headers

app.UseStaticFiles(new StaticFileOptions
{
    OnPrepareResponse = ctx =>
    {
        // Cache 1 năm cho versioned files
        const int durationInSeconds = 60 * 60 * 24 * 365;
        ctx.Context.Response.Headers["Cache-Control"] = 
            $"public, max-age={durationInSeconds}";
    }
});

Response Caching Middleware

// Add response caching
app.UseResponseCaching();

app.UseStaticFiles(new StaticFileOptions
{
    OnPrepareResponse = ctx =>
    {
        ctx.Context.Response.Headers["Cache-Control"] = "public, max-age=3600";
        ctx.Context.Response.Headers["Vary"] = "Accept-Encoding";
    }
});

Security Considerations

Block Sensitive Files

// Block access to sensitive file types
app.UseStaticFiles(new StaticFileOptions
{
    ServeUnknownFileTypes = false, // Default: false
    OnPrepareResponse = ctx =>
    {
        var path = ctx.Context.Request.Path.Value;
        if (path.EndsWith(".json") || path.EndsWith(".config"))
        {
            ctx.Context.Response.StatusCode = 403;
        }
    }
});

Best Practices

┌─────────────────────────────────────────────────────────────────┐
│                    STATIC FILE SECURITY                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ✅ DO:                                                         │
│  - Chỉ serve từ wwwroot hoặc folder chỉ định                    │
│  - Disable directory browsing trong production                  │
│  - Set appropriate cache headers                                │
│  - Use CDN for production static files                         │
│                                                                 │
│  ❌ DON'T:                                                      │
│  - Serve từ folders chứa sensitive data                        │
│  - Enable directory browsing không cần thiết                    │
│  - Serve .config, .json, .env files                            │
│  - Forget to validate file uploads                              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Default Files

Serve Default File

// Serve default.html khi access root
app.UseDefaultFiles(new DefaultFilesOptions
{
    DefaultFileNames = new List<string> { "index.html", "default.html" }
});

// Phải đặt TRƯỚC UseStaticFiles
app.UseStaticFiles();

// http://localhost/ -> wwwroot/index.html

UseFileServer (Combined)

// Kết hợp UseDefaultFiles + UseStaticFiles + UseDirectoryBrowser
app.UseFileServer();

// Hoặc với custom options
app.UseFileServer(new FileServerOptions
{
    FileProvider = new PhysicalFileProvider(
        Path.Combine(builder.Environment.ContentRootPath, "wwwroot")),
    EnableDirectoryBrowsing = false
});

File Upload & Download

Overview Questions

  • Làm sao để upload file trong ASP.NET Core?
  • IFormFile là gì và sử dụng như thế nào?
  • Streaming upload khác buffering như thế nào?
  • Làm sao để handle large file uploads?
  • Download file được implement ra sao?

File Upload với IFormFile

Single File Upload

[HttpPost("upload")]
public async Task<IActionResult> UploadFile(IFormFile file)
{
    if (file == null || file.Length == 0)
    {
        return BadRequest("No file uploaded");
    }

    // Validate file size (max 10MB)
    if (file.Length > 10 * 1024 * 1024)
    {
        return BadRequest("File too large");
    }

    // Validate file type
    var allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif" };
    var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
    if (!allowedExtensions.Contains(extension))
    {
        return BadRequest("Invalid file type");
    }

    // Save file
    var fileName = $"{Guid.NewGuid()}{extension}";
    var filePath = Path.Combine("uploads", fileName);

    using var stream = new FileStream(filePath, FileMode.Create);
    await file.CopyToAsync(stream);

    return Ok(new { fileName, filePath });
}

Multiple File Upload

[HttpPost("upload-multiple")]
public async Task<IActionResult> UploadFiles(List<IFormFile> files)
{
    if (files == null || files.Count == 0)
    {
        return BadRequest("No files uploaded");
    }

    var uploadedFiles = new List<string>();

    foreach (var file in files)
    {
        var fileName = $"{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";
        var filePath = Path.Combine("uploads", fileName);

        using var stream = new FileStream(filePath, FileMode.Create);
        await file.CopyToAsync(stream);

        uploadedFiles.Add(fileName);
    }

    return Ok(uploadedFiles);
}

Upload với Form Data

public class ProductForm
{
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public IFormFile? Image { get; set; }
    public List<IFormFile>? Documents { get; set; }
}

[HttpPost("products")]
public async Task<IActionResult> CreateProduct([FromForm] ProductForm form)
{
    // Process form data
    var product = new Product
    {
        Name = form.Name,
        Price = form.Price
    };

    // Process image
    if (form.Image != null && form.Image.Length > 0)
    {
        var fileName = $"{Guid.NewGuid()}{Path.GetExtension(form.Image.FileName)}";
        var filePath = Path.Combine("uploads", "images", fileName);
        
        using var stream = new FileStream(filePath, FileMode.Create);
        await form.Image.CopyToAsync(stream);
        
        product.ImageUrl = $"/uploads/images/{fileName}";
    }

    // Process documents
    if (form.Documents != null)
    {
        foreach (var doc in form.Documents)
        {
            // Save documents
        }
    }

    return Ok(product);
}

Streaming Upload (Large Files)

Streaming vs Buffering

┌─────────────────────────────────────────────────────────────────┐
│                    UPLOAD METHODS                                │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Buffering (Default):                                           │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐                  │
│  │  Client  │───▶│  Memory  │───▶│   Disk   │                  │
│  │  Upload  │    │  (Form)  │    │  (Save)  │                  │
│  └──────────┘    └──────────┘    └──────────┘                  │
│  - Toàn bộ file load vào memory trước                          │
│  - Phù hợp cho files nhỏ (< 64KB)                              │
│                                                                 │
│  Streaming:                                                     │
│  ┌──────────┐    ┌──────────┐                                  │
│  │  Client  │───▶│   Disk   │                                  │
│  │  Upload  │    │  (Save)  │                                  │
│  └──────────┘    └──────────┘                                  │
│  - Stream trực tiếp vào disk                                   │
│  - Phù hợp cho files lớn                                       │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Streaming Implementation

[HttpPost("upload-stream")]
[RequestSizeLimit(100 * 1024 * 1024)] // 100MB
public async Task<IActionResult> UploadStream()
{
    var fileName = Request.Headers["X-File-Name"].FirstOrDefault();
    if (string.IsNullOrEmpty(fileName))
    {
        return BadRequest("Missing file name header");
    }

    var filePath = Path.Combine("uploads", fileName);

    // Stream trực tiếp vào file
    using var stream = new FileStream(filePath, FileMode.Create);
    await Request.Body.CopyToAsync(stream);

    return Ok(new { fileName, size = stream.Length });
}

Multipart Streaming

[HttpPost("upload-multipart-stream")]
[DisableFormValueModelBinding] // Custom attribute để disable automatic binding
public async Task<IActionResult> UploadMultipart()
{
    if (!Request.HasFormContentType)
    {
        return BadRequest("Invalid content type");
    }

    var form = await Request.ReadFormAsync();
    var file = form.Files["file"];
    
    if (file == null || file.Length == 0)
    {
        return BadRequest("No file");
    }

    var fileName = $"{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";
    var filePath = Path.Combine("uploads", fileName);

    using var stream = new FileStream(filePath, FileMode.Create);
    await file.CopyToAsync(stream);

    return Ok(new { fileName });
}

// Attribute để disable form binding
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        var factories = context.ValueProviderFactories;
        factories.RemoveType<FormValueProviderFactory>();
        factories.RemoveType<FormFileValueProviderFactory>();
        factories.RemoveType<JQueryFormValueProviderFactory>();
    }

    public void OnResourceExecuted(ResourceExecutedContext context) { }
}

File Download

Download từ File

[HttpGet("download/{fileName}")]
public IActionResult Download(string fileName)
{
    var filePath = Path.Combine("uploads", fileName);
    
    if (!System.IO.File.Exists(filePath))
    {
        return NotFound();
    }

    var contentType = GetContentType(fileName);
    return PhysicalFile(filePath, contentType, fileName);
}

private string GetContentType(string fileName)
{
    var extension = Path.GetExtension(fileName).ToLowerInvariant();
    return extension switch
    {
        ".jpg" or ".jpeg" => "image/jpeg",
        ".png" => "image/png",
        ".pdf" => "application/pdf",
        ".txt" => "text/plain",
        _ => "application/octet-stream"
    };
}

Download từ Stream

[HttpGet("download-stream/{fileName}")]
public IActionResult DownloadStream(string fileName)
{
    var filePath = Path.Combine("uploads", fileName);
    
    if (!System.IO.File.Exists(filePath))
    {
        return NotFound();
    }

    var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
    return File(stream, "application/octet-stream", fileName);
}

Download từ Byte Array

[HttpGet("download-bytes")]
public IActionResult DownloadBytes()
{
    var bytes = GeneratePdf(); // Generate file in memory
    return File(bytes, "application/pdf", "report.pdf");
}

Configuration

File Size Limits

// Program.cs
builder.Services.Configure<FormOptions>(options =>
{
    options.MultipartBodyLengthLimit = 100 * 1024 * 1024; // 100MB
    options.ValueLengthLimit = int.MaxValue;
    options.MultipartHeadersLengthLimit = int.MaxValue;
});

// Hoặc trong appsettings.json
{
  "Kestrel": {
    "Limits": {
      "MaxRequestBodySize": 104857600 // 100MB
    }
  }
}

Request Size Limit Attribute

// Per-action limit
[HttpPost("upload")]
[RequestSizeLimit(50 * 1024 * 1024)] // 50MB
public async Task<IActionResult> Upload(IFormFile file)
{
    // ...
}

// No limit
[HttpPost("upload-large")]
[DisableRequestSizeLimit]
public async Task<IActionResult> UploadLarge(IFormFile file)
{
    // ...
}

Best Practices

1. Validate File Uploads

public class FileUploadValidator
{
    private static readonly Dictionary<string, string[]> AllowedExtensions = new()
    {
        ["image"] = new[] { ".jpg", ".jpeg", ".png", ".gif", ".webp" },
        ["document"] = new[] { ".pdf", ".doc", ".docx" },
        ["video"] = new[] { ".mp4", ".avi", ".mov" }
    };

    public static (bool IsValid, string? Error) Validate(IFormFile file, string category)
    {
        if (file == null || file.Length == 0)
            return (false, "No file uploaded");

        if (file.Length > 100 * 1024 * 1024)
            return (false, "File too large (max 100MB)");

        var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
        if (!AllowedExtensions.TryGetValue(category, out var allowed) || 
            !allowed.Contains(extension))
        {
            return (false, $"Invalid file type. Allowed: {string.Join(", ", allowed)}");
        }

        return (true, null);
    }
}

2. Use Safe File Names

// ✅ Good - generate safe file name
var safeFileName = $"{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";

// ❌ Bad - use original file name (security risk)
var fileName = file.FileName; // Could contain path traversal

3. Scan for Malware

// Integrate with antivirus scanning
public async Task<bool> ScanForMalwareAsync(Stream fileStream)
{
    // Use antivirus API or external service
    // Return true if clean
    return true;
}

AWS S3 File Operations

Overview Questions

  • Làm sao để upload file lên AWS S3?
  • Làm sao để download file từ S3?
  • Làm sao để delete file trong S3?
  • Presigned URL là gì và khi nào sử dụng?
  • Cách cấu hình S3 client trong ASP.NET Core?

AWS S3 Configuration

Cài đặt Package

dotnet add package AWSSDK.S3

Cấu hình trong Program.cs

// Program.cs
builder.Services.AddAWSService<IAmazonS3>(new AmazonS3Config
{
    RegionEndpoint = RegionEndpoint.USEast1
});

// Hoặc sử dụng options pattern
builder.Services.Configure<AwsS3Options>(
    builder.Configuration.GetSection("AWS:S3"));

// Cấu hình trong appsettings.json
/*
{
  "AWS": {
    "S3": {
      "BucketName": "my-bucket",
      "Region": "us-east-1"
    }
  }
}
*/

Cấu hình với Credentials

// Sử dụng AWS credentials
var credentials = new BasicAWSCredentials("access-key", "secret-key");

builder.Services.AddAWSService<IAmazonS3>(new AmazonS3Config
{
    RegionEndpoint = RegionEndpoint.USEast1
}, credentials);

// Hoặc sử dụng IAM role (trên EC2, Lambda)
builder.Services.AddAWSService<IAmazonS3>();

Upload Files

Simple Upload

public class S3Service
{
    private readonly IAmazonS3 _s3Client;

    public S3Service(IAmazonS3 s3Client)
    {
        _s3Client = s3Client;
    }

    public async Task<string> UploadFileAsync(Stream fileStream, string key)
    {
        var request = new PutObjectRequest
        {
            BucketName = "my-bucket",
            Key = key,
            InputStream = fileStream,
            ContentType = "application/octet-stream"
        };

        var response = await _s3Client.PutObjectAsync(request);
        
        return $"https://my-bucket.s3.amazonaws.com/{key}";
    }
}

Upload với Metadata

public async Task<string> UploadWithMetadataAsync(
    Stream fileStream, 
    string key, 
    string contentType,
    Dictionary<string, string> metadata)
{
    var request = new PutObjectRequest
    {
        BucketName = "my-bucket",
        Key = key,
        InputStream = fileStream,
        ContentType = contentType
    };

    // Thêm metadata
    foreach (var item in metadata)
    {
        request.Metadata[item.Key] = item.Value;
    }

    await _s3Client.PutObjectAsync(request);
    return key;
}

Upload Large Files (Multipart)

public async Task<string> UploadLargeFileAsync(string filePath, string key)
{
    var fileInfo = new FileInfo(filePath);
    const int partSize = 5 * 1024 * 1024; // 5MB

    var initiateRequest = new CreateMultipartUploadRequest
    {
        BucketName = "my-bucket",
        Key = key,
        ContentType = "application/octet-stream"
    };

    var initiateResponse = await _s3Client.CreateMultipartUploadAsync(initiateRequest);

    var uploadParts = new List<UploadPartResponse>();
    var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
    var buffer = new byte[partSize];
    var partNumber = 1;

    try
    {
        while (fileStream.Position < fileInfo.Length)
        {
            var bytesRead = await fileStream.ReadAsync(buffer, 0, partSize);
            var partRequest = new UploadPartRequest
            {
                BucketName = "my-bucket",
                Key = key,
                UploadId = initiateResponse.UploadId,
                PartNumber = partNumber,
                InputStream = new MemoryStream(buffer, 0, bytesRead),
                PartSize = bytesRead
            };

            var uploadResponse = await _s3Client.UploadPartAsync(partRequest);
            uploadParts.Add(uploadResponse);
            partNumber++;
        }

        var completeRequest = new CompleteMultipartUploadRequest
        {
            BucketName = "my-bucket",
            Key = key,
            UploadId = initiateResponse.UploadId,
            PartETags = uploadParts.Select((p, i) => new PartETag(i + 1, p.ETag)).ToList()
        };

        await _s3Client.CompleteMultipartUploadAsync(completeRequest);
        return key;
    }
    catch (Exception)
    {
        await _s3Client.AbortMultipartUploadAsync(new AbortMultipartUploadRequest
        {
            BucketName = "my-bucket",
            Key = key,
            UploadId = initiateResponse.UploadId
        });
        throw;
    }
    finally
    {
        fileStream.Dispose();
    }
}

Download Files

Download to Stream

public async Task<Stream> DownloadFileAsync(string key)
{
    var request = new GetObjectRequest
    {
        BucketName = "my-bucket",
        Key = key
    };

    var response = await _s3Client.GetObjectAsync(request);
    return response.ResponseStream;
}

Download to Local File

public async Task DownloadToFileAsync(string key, string localPath)
{
    var request = new GetObjectRequest
    {
        BucketName = "my-bucket",
        Key = key
    };

    var response = await _s3Client.GetObjectAsync(request);
    
    using var writer = new FileStream(localPath, FileMode.Create);
    await response.ResponseStream.CopyToAsync(writer);
}

Get File URL

public string GetFileUrl(string key, TimeSpan? expiry = null)
{
    var request = new GetPreSignedUrlRequest
    {
        BucketName = "my-bucket",
        Key = key,
        Expires = DateTime.UtcNow.Add(expiry ?? TimeSpan.FromHours(1))
    };

    return _s3Client.GetPreSignedURL(request);
}

Delete Files

Delete Single File

public async Task DeleteFileAsync(string key)
{
    var request = new DeleteObjectRequest
    {
        BucketName = "my-bucket",
        Key = key
    };

    await _s3Client.DeleteObjectAsync(request);
}

Delete Multiple Files

public async Task DeleteFilesAsync(List<string> keys)
{
    var objects = keys.Select(key => new KeyVersion { Key = key }).ToList();
    
    var request = new DeleteObjectsRequest
    {
        BucketName = "my-bucket",
        Objects = objects
    };

    var response = await _s3Client.DeleteObjectsAsync(request);
}

Delete Folder (Prefix)

public async Task DeleteFolderAsync(string prefix)
{
    // List all objects with the prefix
    var listRequest = new ListObjectsV2Request
    {
        BucketName = "my-bucket",
        Prefix = prefix
    };

    var objectsToDelete = new List<KeyVersion>();

    var response = await _s3Client.ListObjectsV2Async(listRequest);
    
    foreach (var s3Object in response.S3Objects)
    {
        objectsToDelete.Add(new KeyVersion { Key = s3Object.Key });
    }

    if (objectsToDelete.Any())
    {
        var deleteRequest = new DeleteObjectsRequest
        {
            BucketName = "my-bucket",
            Objects = objectsToDelete
        };

        await _s3Client.DeleteObjectsAsync(deleteRequest);
    }
}

List Files

List All Files

public async Task<List<S3Object>> ListFilesAsync(string prefix = "")
{
    var request = new ListObjectsV2Request
    {
        BucketName = "my-bucket",
        Prefix = prefix
    };

    var response = await _s3Client.ListObjectsV2Async(request);
    return response.S3Objects.ToList();
}

List Files with Pagination

public async Task<List<S3Object>> ListAllFilesAsync(string prefix = "")
{
    var files = new List<S3Object>();
    var request = new ListObjectsV2Request
    {
        BucketName = "my-bucket",
        Prefix = prefix
    };

    ListObjectsV2Response response;
    do
    {
        response = await _s3Client.ListObjectsV2Async(request);
        files.AddRange(response.S3Objects);
        request.ContinuationToken = response.NextContinuationToken;
    } while (response.IsTruncated);

    return files;
}

Presigned URLs

Generate Presigned URL

public string GeneratePresignedUrl(string key, TimeSpan expiry)
{
    var request = new GetPreSignedUrlRequest
    {
        BucketName = "my-bucket",
        Key = key,
        Expires = DateTime.UtcNow.Add(expiry)
    };

    return _s3Client.GetPreSignedURL(request);
}

Generate Upload URL (For Direct Browser Upload)

public string GenerateUploadUrl(string key, string contentType, TimeSpan expiry)
{
    var request = new PutObjectRequest
    {
        BucketName = "my-bucket",
        Key = key,
        ContentType = contentType
    };

    // Get presigned URL for upload
    return _s3Client.GetPreSignedURL(request);
}

Best Practices

1. Use Appropriate Storage Class

// Sử dụng S3 Standard cho frequently accessed files
var request = new PutObjectRequest
{
    BucketName = "my-bucket",
    Key = key,
    StorageClass = S3StorageClass.Standard
};

// Sử dụng S3 Standard-IA cho infrequently accessed files
request.StorageClass = S3StorageClass.StandardInfrequentAccess;

// Sử dụng S3 Glacier cho archival files
request.StorageClass = S3StorageClass.Glacier;

2. Enable Encryption

// Server-side encryption với AWS-managed key
var request = new PutObjectRequest
{
    BucketName = "my-bucket",
    Key = key,
    ServerSideEncryptionMethod = ServerSideEncryptionMethod.AES256
};

// Hoặc với KMS key
request.ServerSideEncryptionMethod = ServerSideEncryptionMethod.AWSKMS;
request.ServerSideEncryptionKeyManagementServiceKeyId = kmsKeyId;

3. Set Proper Permissions

// IAM Policy example
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "s3:PutObject",
                "s3:DeleteObject"
            ],
            "Resource": "arn:aws:s3:::my-bucket/*"
        }
    ]
}

4. Handle Errors Properly

try
{
    await _s3Client.PutObjectAsync(request);
}
catch (AmazonS3Exception ex)
{
    // Handle S3-specific errors
    Console.WriteLine($"S3 Error: {ex.ErrorCode} - {ex.Message}");
    throw;
}
catch (Exception ex)
{
    // Handle general errors
    Console.WriteLine($"Error: {ex.Message}");
    throw;
}

Hosted Services & Background Tasks

Overview Questions

  • Hosted Service là gì và khi nào cần sử dụng?
  • IHostedServiceBackgroundService khác nhau như thế nào?
  • Làm sao để thực thi task theo schedule?
  • Quản lý cancellation token trong background tasks ra sao?
  • Worker Service là gì và cách tạo?

IHostedService

Interface

public interface IHostedService
{
    Task StartAsync(CancellationToken cancellationToken);
    Task StopAsync(CancellationToken cancellationToken);
}

Implementation

public class TimedHostedService : IHostedService, IDisposable
{
    private readonly ILogger<TimedHostedService> _logger;
    private Timer? _timer;

    public TimedHostedService(ILogger<TimedHostedService> logger)
    {
        _logger = logger;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Timed Hosted Service running.");
        
        // Chạy mỗi 1 phút
        _timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromMinutes(1));
        
        return Task.CompletedTask;
    }

    private void DoWork(object? state)
    {
        _logger.LogInformation("Timed Hosted Service is working.");
        // Thực thi công việc background
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Timed Hosted Service is stopping.");
        
        _timer?.Change(Timeout.Infinite, 0);
        
        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _timer?.Dispose();
    }
}

// Đăng ký
builder.Services.AddHostedService<TimedHostedService>();

BackgroundService

Abstract Class

// BackgroundService là implementation base của IHostedService
// Cung cấp ExecuteAsync method dễ sử dụng hơn
public abstract class BackgroundService : IHostedService, IDisposable
{
    protected abstract Task ExecuteAsync(CancellationToken stoppingToken);
    
    public virtual Task StartAsync(CancellationToken cancellationToken);
    public virtual Task StopAsync(CancellationToken cancellationToken);
    public virtual void Dispose();
}

Implementation

public class QueueProcessorService : BackgroundService
{
    private readonly ILogger<QueueProcessorService> _logger;
    private readonly Channel<string> _channel;

    public QueueProcessorService(
        ILogger<QueueProcessorService> logger,
        Channel<string> channel)
    {
        _logger = logger;
        _channel = channel;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Queue Processor Service starting");

        await foreach (var item in _channel.Reader.ReadAllAsync(stoppingToken))
        {
            try
            {
                _logger.LogInformation("Processing: {Item}", item);
                await ProcessItemAsync(item, stoppingToken);
            }
            catch (Exception ex) when (!stoppingToken.IsCancellationRequested)
            {
                _logger.LogError(ex, "Error processing item: {Item}", item);
            }
        }

        _logger.LogInformation("Queue Processor Service stopping");
    }

    private async Task ProcessItemAsync(string item, CancellationToken token)
    {
        // Simulate processing
        await Task.Delay(100, token);
    }
}

// Đăng ký
builder.Services.AddSingleton(Channel.CreateUnbounded<string>());
builder.Services.AddHostedService<QueueProcessorService>();

Periodic Background Service

Pattern với Timer

public class PeriodicService : BackgroundService
{
    private readonly ILogger<PeriodicService> _logger;
    private readonly TimeSpan _period = TimeSpan.FromSeconds(30);

    public PeriodicService(ILogger<PeriodicService> logger)
    {
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                _logger.LogInformation("Doing periodic work at {Time}", DateTimeOffset.UtcNow);
                await DoWorkAsync(stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error in periodic work");
            }

            await Task.Delay(_period, stoppingToken);
        }
    }

    private async Task DoWorkAsync(CancellationToken token)
    {
        // Thực thi công việc
        await Task.CompletedTask;
    }
}

Scheduled Tasks

Cron-based Scheduler

dotnet add package Coravel
// Program.cs
builder.Services.AddScheduler();

// Tạo scheduled task
public class CleanupTask : IInvocable
{
    private readonly ILogger<CleanupTask> _logger;

    public CleanupTask(ILogger<CleanupTask> logger)
    {
        _logger = logger;
    }

    public async Task Invoke()
    {
        _logger.LogInformation("Running cleanup task");
        // Cleanup logic
    }
}

// Đăng ký schedule
builder.Services.AddTransient<CleanupTask>();

// Trong Program.cs
var app = builder.Build();
app.Services.UseScheduler(scheduler =>
{
    scheduler.Schedule<CleanupTask>()
        .EveryFiveMinutes()
        .PreventOverlapping();
});

app.Run();

Hangfire

dotnet add package Hangfire
// Program.cs
builder.Services.AddHangfire(config =>
    config.UseSqlServerStorage(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddHangfireServer();

var app = builder.Build();
app.UseHangfireDashboard();

// Recurring job
RecurringJob.AddOrUpdate<ICleanupService>(
    "cleanup",
    service => service.Cleanup(),
    Cron.Daily);

app.Run();

Worker Service Template

Tạo Worker Service

dotnet new worker -n MyWorker

Structure

// Program.cs
using MyWorker;

var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();

var host = builder.Build();
host.Run();

// Worker.cs
namespace MyWorker;

public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;

    public Worker(ILogger<Worker> logger)
    {
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
            await Task.Delay(1000, stoppingToken);
        }
    }
}

Run as Windows Service

dotnet add package Microsoft.Extensions.Hosting.WindowsServices
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();

// Add Windows Service support
builder.Services.AddWindowsService(options =>
{
    options.ServiceName = "My Worker Service";
});

var host = builder.Build();
host.Run();

Best Practices

1. Handle Cancellation Properly

// ✅ Good
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    while (!stoppingToken.IsCancellationRequested)
    {
        await DoWorkAsync(stoppingToken);
        await Task.Delay(1000, stoppingToken);
    }
}

// ❌ Bad - ignore cancellation
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    while (true) // Infinite loop, no cancellation check
    {
        await DoWorkAsync(CancellationToken.None);
    }
}

2. Handle Exceptions

// ✅ Good - catch và log exceptions
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    while (!stoppingToken.IsCancellationRequested)
    {
        try
        {
            await DoWorkAsync(stoppingToken);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error in background service");
        }
        await Task.Delay(1000, stoppingToken);
    }
}

3. Use Scoped Services Correctly

// ✅ Good - tạo scope cho scoped services
public class ScopedWorker : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;
    private readonly ILogger<ScopedWorker> _logger;

    public ScopedWorker(IServiceProvider serviceProvider, ILogger<ScopedWorker> logger)
    {
        _serviceProvider = serviceProvider;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            // Tạo scope mới cho mỗi iteration
            using var scope = _serviceProvider.CreateScope();
            var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
            
            await ProcessAsync(dbContext, stoppingToken);
            
            await Task.Delay(5000, stoppingToken);
        }
    }
}

Health Checks

Overview Questions

  • Health Checks là gì và tại sao cần thiết?
  • Làm sao để cấu hình health check endpoints?
  • Custom health check được viết như thế nào?
  • Health check UI là gì và cách tích hợp?
  • Health check response format ra sao?

Basic Health Checks

Configuration

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Add health checks
builder.Services.AddHealthChecks();

var app = builder.Build();

// Map health check endpoint
app.MapHealthChecks("/health");

// Basic health check
app.MapGet("/health/basic", () => Results.Ok("Healthy"));

app.Run();

Response

// Healthy
{
  "status": "Healthy"
}

// Unhealthy
{
  "status": "Unhealthy",
  "results": {
    "database": {
      "status": "Unhealthy",
      "description": "Connection failed"
    }
  }
}

Built-in Health Checks

Database Health Check

dotnet add package AspNetCore.HealthChecks.SqlServer
builder.Services.AddHealthChecks()
    .AddSqlServer(
        builder.Configuration.GetConnectionString("Default"),
        name: "sqlserver",
        failureStatus: HealthStatus.Unhealthy,
        tags: new[] { "db", "sql" });

Redis Health Check

dotnet add package AspNetCore.HealthChecks.Redis
builder.Services.AddHealthChecks()
    .AddRedis(
        "localhost:6379",
        name: "redis",
        failureStatus: HealthStatus.Degraded);

HTTP Endpoint Health Check

builder.Services.AddHealthChecks()
    .AddUrlGroup(
        new Uri("https://api.external.com/health"),
        name: "external-api",
        failureStatus: HealthStatus.Degraded);

Multiple Health Checks

builder.Services.AddHealthChecks()
    .AddSqlServer(
        builder.Configuration.GetConnectionString("Default"),
        name: "database")
    .AddRedis("localhost:6379", name: "cache")
    .AddUrlGroup(new Uri("https://api.external.com"), name: "external");

Custom Health Checks

IHealthCheck Implementation

public class DiskSpaceHealthCheck : IHealthCheck
{
    private readonly ILogger<DiskSpaceHealthCheck> _logger;
    private readonly long _thresholdBytes = 1024 * 1024 * 100; // 100MB

    public DiskSpaceHealthCheck(ILogger<DiskSpaceHealthCheck> logger)
    {
        _logger = logger;
    }

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        var drive = new DriveInfo(Path.GetPathRoot(Directory.GetCurrentDirectory())!);
        var freeSpace = drive.AvailableFreeSpace;

        if (freeSpace > _thresholdBytes)
        {
            return HealthCheckResult.Healthy(
                $"Free disk space: {freeSpace / 1024 / 1024}MB");
        }

        return HealthCheckResult.Unhealthy(
            $"Low disk space: {freeSpace / 1024 / 1024}MB remaining");
    }
}

// Đăng ký
builder.Services.AddHealthChecks()
    .AddCheck<DiskSpaceHealthCheck>("disk-space");

Delegate-based Health Check

builder.Services.AddHealthChecks()
    .AddCheck("memory", () =>
    {
        var memoryUsed = GC.GetGCMemoryInfo().HeapSizeBytes;
        var memoryLimit = 512L * 1024 * 1024; // 512MB

        return memoryUsed < memoryLimit
            ? HealthCheckResult.Healthy($"Memory: {memoryUsed / 1024 / 1024}MB")
            : HealthCheckResult.Unhealthy($"High memory: {memoryUsed / 1024 / 1024}MB");
    });

Health Check Options

Response Writer

app.MapHealthChecks("/health", new HealthCheckOptions
{
    // Custom response writer
    ResponseWriter = async (context, report) =>
    {
        var result = new
        {
            status = report.Status.ToString(),
            duration = report.TotalDuration,
            checks = report.Entries.Select(e => new
            {
                name = e.Key,
                status = e.Value.Status.ToString(),
                description = e.Value.Description,
                data = e.Value.Data
            }),
            timestamp = DateTime.UtcNow
        };

        context.Response.ContentType = "application/json";
        await context.Response.WriteAsJsonAsync(result);
    }
});

Predicate & Tags

// Chỉ check database
app.MapHealthChecks("/health/db", new HealthCheckOptions
{
    Predicate = (check) => check.Tags.Contains("db")
});

// Chỉ check cache
app.MapHealthChecks("/health/cache", new HealthCheckOptions
{
    Predicate = (check) => check.Tags.Contains("cache")
});

Status Code Mapping

app.MapHealthChecks("/health", new HealthCheckOptions
{
    ResultStatusCodes = new Dictionary<HealthStatus, int>
    {
        [HealthStatus.Healthy] = 200,
        [HealthStatus.Degraded] = 200,
        [HealthStatus.Unhealthy] = 503
    }
});

Health Check UI

Setup

dotnet add package AspNetCore.HealthChecks.UI
dotnet add package AspNetCore.HealthChecks.UI.InMemory.Storage
// Program.cs
builder.Services.AddHealthChecksUI()
    .AddInMemoryStorage();

var app = builder.Build();

app.MapHealthChecks("/health");
app.MapHealthChecksUI("/health-ui");

app.Run();

Configuration

{
  "HealthChecksUI": {
    "HealthChecks": [
      {
        "Name": "API Health",
        "Uri": "https://localhost:5001/health"
      }
    ],
    "EvaluationTimeInSeconds": 30,
    "MinimumSecondsBetweenFailureNotifications": 60
  }
}

Kubernetes Probes

Liveness & Readiness

// Liveness - App is running
app.MapHealthChecks("/healthz", new HealthCheckOptions
{
    Predicate = _ => false // No checks, just return 200
});

// Readiness - App is ready to receive traffic
app.MapHealthChecks("/ready", new HealthCheckOptions
{
    Predicate = check => true // Run all checks
});

// Startup - App has started
app.MapHealthChecks("/started", new HealthCheckOptions
{
    Predicate = _ => false
});

Kubernetes Config

livenessProbe:
  httpGet:
    path: /healthz
    port: 80
  initialDelaySeconds: 30
  periodSeconds: 10

readinessProbe:
  httpGet:
    path: /ready
    port: 80
  initialDelaySeconds: 10
  periodSeconds: 5

Best Practices

1. Use Appropriate Checks

// ✅ Good - lightweight checks
builder.Services.AddHealthChecks()
    .AddSqlServer(connectionString, name: "database")
    .AddRedis(redisConnection, name: "cache");

// ❌ Bad - expensive checks
builder.Services.AddHealthChecks()
    .AddCheck("full-database-scan", async () => 
    {
        // Expensive query - avoid in health check
        var count = await db.Products.CountAsync();
        return count > 0 ? HealthCheckResult.Healthy() : HealthCheckResult.Unhealthy();
    });

2. Separate Liveness and Readiness

┌─────────────────────────────────────────────────────────────────┐
│                    HEALTH CHECK TYPES                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Liveness (/healthz):                                           │
│  - App is running                                               │
│  - No deadlock or infinite loop                                 │
│  - Kubernetes restarts if failed                                │
│                                                                 │
│  Readiness (/ready):                                            │
│  - App can handle requests                                      │
│  - Dependencies are available                                   │
│  - Kubernetes removes from service if failed                    │
│                                                                 │
│  Startup (/started):                                            │
│  - App has finished initialization                              │
│  - Used during startup probe                                    │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

3. Don’t Overload Health Checks

// ✅ Good - simple checks
builder.Services.AddHealthChecks()
    .AddSqlServer(connectionString, name: "db")
    .AddRedis(redisConnection, name: "cache");

// ❌ Bad - too many external dependencies
builder.Services.AddHealthChecks()
    .AddSqlServer(connectionString)
    .AddRedis(redisConnection)
    .AddUrlGroup(externalApi1)
    .AddUrlGroup(externalApi2)
    .AddUrlGroup(externalApi3); // External APIs có thể down

Minimal APIs Advanced

Overview Questions

  • Route Groups là gì và khi nào sử dụng?
  • Endpoint Filters hoạt động như thế nào?
  • Làm sao để organize large Minimal APIs?
  • Parameter binding trong Minimal APIs có gì đặc biệt?
  • So sánh Minimal APIs vs Controllers khi nào dùng cái nào?

Route Groups (.NET 7+)

Basic Route Groups

var app = builder.Build();

// Group với common prefix
var products = app.MapGroup("/api/products");

products.MapGet("/", () => new[] { "Product 1", "Product 2" });
products.MapGet("/{id}", (int id) => new { Id = id, Name = "Product" });
products.MapPost("/", (Product product) => Results.Created($"/api/products/{product.Id}", product));
products.MapPut("/{id}", (int id, Product product) => Results.NoContent());
products.MapDelete("/{id}", (int id) => Results.NoContent());

app.Run();

Route Groups với Common Configuration

var app = builder.Build();

// Group với common middleware
var products = app.MapGroup("/api/products")
    .RequireAuthorization()
    .WithTags("Products")
    .WithOpenApi();

products.MapGet("/", () => new[] { "Product 1" });
products.MapGet("/{id}", (int id) => new { Id = id });
products.MapPost("/", (Product product) => Results.Created($"/api/products/{product.Id}", product));

app.Run();

Nested Route Groups

var app = builder.Build();

var api = app.MapGroup("/api")
    .RequireAuthorization();

var products = api.MapGroup("/products")
    .WithTags("Products");

products.MapGet("/", () => new[] { "Product 1" });
products.MapGet("/{id}", (int id) => new { Id = id });

var orders = api.MapGroup("/orders")
    .WithTags("Orders");

orders.MapGet("/", () => new[] { "Order 1" });
orders.MapGet("/{id}", (int id) => new { Id = id });

app.Run();

Endpoint Filters (.NET 7+)

Basic Filter

var app = builder.Build();

app.MapGet("/api/products/{id}", (int id, AppDbContext db) =>
{
    return db.Products.Find(id);
})
.AddEndpointFilter(async (context, next) =>
{
    var id = context.GetArgument<int>(0);
    
    if (id <= 0)
    {
        return Results.BadRequest("Invalid ID");
    }
    
    return await next(context);
});

app.Run();

Logging Filter

public class LoggingFilter : IEndpointFilter
{
    private readonly ILogger<LoggingFilter> _logger;

    public LoggingFilter(ILogger<LoggingFilter> logger)
    {
        _logger = logger;
    }

    public async ValueTask<object?> InvokeAsync(
        EndpointFilterInvocationContext context,
        EndpointFilterDelegate next)
    {
        var startTime = DateTime.UtcNow;
        _logger.LogInformation("Starting {Method} {Path}", 
            context.HttpContext.Request.Method,
            context.HttpContext.Request.Path);

        var result = await next(context);

        _logger.LogInformation("Completed in {Elapsed}ms", 
            (DateTime.UtcNow - startTime).TotalMilliseconds);

        return result;
    }
}

// Đăng ký
app.MapGet("/api/products", async (AppDbContext db) =>
{
    return await db.Products.ToListAsync();
})
.AddEndpointFilter<LoggingFilter>();

Validation Filter

public class ValidationFilter<T> : IEndpointFilter
{
    public async ValueTask<object?> InvokeAsync(
        EndpointFilterInvocationContext context,
        EndpointFilterDelegate next)
    {
        // Find the argument to validate
        for (var i = 0; i < context.Arguments.Count; i++)
        {
            if (context.Arguments[i] is T item)
            {
                var validationContext = new ValidationContext<T>(item);
                var validator = context.HttpContext.RequestServices
                    .GetRequiredService<IValidator<T>>();
                var result = await validator.ValidateAsync(validationContext);

                if (!result.IsValid)
                {
                    var errors = result.Errors
                        .GroupBy(e => e.PropertyName)
                        .ToDictionary(
                            g => g.Key,
                            g => g.Select(e => e.ErrorMessage).ToArray());

                    return Results.ValidationProblem(errors);
                }
            }
        }

        return await next(context);
    }
}

// Sử dụng
app.MapPost("/api/products", (Product product, AppDbContext db) =>
{
    db.Products.Add(product);
    db.SaveChanges();
    return Results.Created($"/api/products/{product.Id}", product);
})
.AddEndpointFilter<ValidationFilter<Product>>();

Parameter Binding

Built-in Binding

// FromRoute
app.MapGet("/api/products/{id}", (int id) => id);

// FromQuery
app.MapGet("/api/products", (int page, int pageSize) => new { page, pageSize });

// FromBody
app.MapPost("/api/products", (Product product) => product);

// FromServices
app.MapGet("/api/products", (AppDbContext db) => db.Products.ToList());

// FromHeader
app.MapGet("/api/secure", ([FromHeader(Name = "X-API-Key")] string apiKey) => apiKey);

// HttpContext
app.MapGet("/api/context", (HttpContext context) => context.Request.Path);

// HttpRequest/HttpResponse
app.MapGet("/api/request", (HttpRequest request) => request.Headers);

Custom Binding

// Bind từ query string vào complex type
app.MapGet("/api/products", ([AsParameters] PagingParams p) => 
{
    return new { p.Page, p.PageSize, p.Search };
});

public record PagingParams(
    int Page = 1,
    int PageSize = 10,
    string? Search = null);

TryParse Binding

// Custom type với TryParse
public class ProductId
{
    public int Value { get; }

    public ProductId(int value) => Value = value;

    public static bool TryParse(string? value, out ProductId? result)
    {
        if (int.TryParse(value, out var id))
        {
            result = new ProductId(id);
            return true;
        }
        result = null;
        return false;
    }
}

// Binding tự động qua TryParse
app.MapGet("/api/products/{id}", (ProductId id) => new { id.Value });

Organizing Large Minimal APIs

Extension Methods Pattern

// Program.cs
var app = builder.Build();

app.MapProductEndpoints();
app.MapOrderEndpoints();
app.MapUserEndpoints();

app.Run();

// ProductEndpoints.cs
public static class ProductEndpoints
{
    public static void MapProductEndpoints(this IEndpointRouteBuilder routes)
    {
        var group = routes.MapGroup("/api/products")
            .WithTags("Products")
            .WithOpenApi();

        group.MapGet("/", async (AppDbContext db) => 
            await db.Products.ToListAsync());

        group.MapGet("/{id}", async (int id, AppDbContext db) =>
            await db.Products.FindAsync(id) is Product product
                ? Results.Ok(product)
                : Results.NotFound());

        group.MapPost("/", async (Product product, AppDbContext db) =>
        {
            db.Products.Add(product);
            await db.SaveChangesAsync();
            return Results.Created($"/api/products/{product.Id}", product);
        });

        group.MapPut("/{id}", async (int id, Product product, AppDbContext db) =>
        {
            var existing = await db.Products.FindAsync(id);
            if (existing == null) return Results.NotFound();
            
            existing.Name = product.Name;
            existing.Price = product.Price;
            await db.SaveChangesAsync();
            return Results.NoContent();
        });

        group.MapDelete("/{id}", async (int id, AppDbContext db) =>
        {
            var product = await db.Products.FindAsync(id);
            if (product == null) return Results.NotFound();
            
            db.Products.Remove(product);
            await db.SaveChangesAsync();
            return Results.NoContent();
        });
    }
}

Interface-based Pattern

public interface IEndpoint
{
    void MapEndpoint(IEndpointRouteBuilder app);
}

public class ProductEndpoint : IEndpoint
{
    public void MapEndpoint(IEndpointRouteBuilder app)
    {
        var group = app.MapGroup("/api/products");
        group.MapGet("/", () => new[] { "Product 1" });
        group.MapGet("/{id}", (int id) => new { Id = id });
    }
}

// Program.cs
var app = builder.Build();

var scope = app.Services.CreateScope();
var endpoints = scope.ServiceProvider.GetServices<IEndpoint>();

foreach (var endpoint in endpoints)
{
    endpoint.MapEndpoint(app);
}

app.Run();

Minimal APIs vs Controllers

Comparison

AspectMinimal APIsControllers
Code sizeÍt hơnNhiều hơn
ComplexityĐơn giảnPhức tạp hơn
FeaturesĐầy đủ cho most casesFull MVC features
FiltersEndpoint FiltersAction/Result Filters
Model BindingTự độngAttribute-based
TestingKhó hơnDễ hơn
OrganizationExtension methodsControllers folder
Use caseSmall APIs, microservicesLarge applications

When to Use

┌─────────────────────────────────────────────────────────────────┐
│                    WHEN TO USE                                   │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Minimal APIs:                                                  │
│  - Microservices                                                │
│  - Small to medium APIs                                         │
│  - Quick prototypes                                             │
│  - Serverless functions                                         │
│  - Simple CRUD operations                                       │
│                                                                 │
│  Controllers:                                                   │
│  - Large enterprise applications                                │
│  - Complex business logic                                       │
│  - Need MVC features (Views, Razor Pages)                       │
│  - Team với nhiều developers                                    │
│  - Need extensive filter pipeline                               │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Content Negotiation

Overview Questions

  • Content Negotiation là gì và tại sao cần thiết?
  • ASP.NET Core xử lý input/output formatting như thế nào?
  • Làm sao để thêm XML support?
  • Custom formatter được viết ra sao?
  • Response format negotiation hoạt động như thế nào?

Content Negotiation Basics

What is Content Negotiation?

┌─────────────────────────────────────────────────────────────────┐
│                CONTENT NEGOTIATION FLOW                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Client Request:                                                │
│  Accept: application/json                                       │
│  Content-Type: application/json                                 │
│         │                                                       │
│         ▼                                                       │
│  ┌─────────────────────────────┐                               │
│  │    Input Formatter          │                               │
│  │  (JSON, XML, etc.)          │                               │
│  └─────────────┬───────────────┘                               │
│                │                                               │
│                ▼                                               │
│  ┌─────────────────────────────┐                               │
│  │       Controller            │                               │
│  └─────────────┬───────────────┘                               │
│                │                                               │
│                ▼                                               │
│  ┌─────────────────────────────┐                               │
│  │    Output Formatter         │                               │
│  │  (JSON, XML, etc.)          │                               │
│  └─────────────┬───────────────┘                               │
│                │                                               │
│                ▼                                               │
│  Client Response:                                               │
│  Content-Type: application/json                                 │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Default Configuration

// ASP.NET Core mặc định chỉ support JSON
builder.Services.AddControllers();

// JSON là default formatter
// System.Text.Json được sử dụng

Input Formatters

JSON Input (Default)

// Request
// POST /api/products
// Content-Type: application/json
// {"name": "Laptop", "price": 999.99}

[HttpPost]
public IActionResult Create(Product product)
{
    // product được deserialize từ JSON
    return Ok(product);
}

XML Input

dotnet add package Microsoft.AspNetCore.Mvc.NewtonsoftJson
builder.Services.AddControllers()
    .AddXmlSerializerFormatters(); // Add XML support

// Request
// POST /api/products
// Content-Type: application/xml
// <Product><Name>Laptop</Name><Price>999.99</Price></Product>

[HttpPost]
public IActionResult Create(Product product)
{
    return Ok(product);
}

Custom Input Formatter

public class CsvInputFormatter : InputFormatter
{
    public CsvInputFormatter()
    {
        SupportedMediaTypes.Add("text/csv");
    }

    protected override bool CanReadType(Type type)
    {
        return type == typeof(List<Product>);
    }

    public override async Task<InputFormatterResult> ReadRequestBodyAsync(
        InputFormatterContext context)
    {
        var request = context.HttpContext.Request;
        using var reader = new StreamReader(request.Body);
        var csv = await reader.ReadToEndAsync();
        
        var products = ParseCsv(csv);
        
        return await InputFormatterResult.SuccessAsync(products);
    }

    private List<Product> ParseCsv(string csv)
    {
        // Parse CSV logic
        return new List<Product>();
    }
}

// Đăng ký
builder.Services.AddControllers(options =>
{
    options.InputFormatters.Add(new CsvInputFormatter());
});

Output Formatters

JSON Output (Default)

[HttpGet]
public IActionResult Get()
{
    return Ok(new { name = "Product", price = 99.99 });
}

// Response
// Content-Type: application/json
// {"name":"Product","price":99.99}

XML Output

builder.Services.AddControllers()
    .AddXmlSerializerFormatters();

[HttpGet]
[Produces("application/xml")]
public IActionResult Get()
{
    return Ok(new Product { Name = "Product", Price = 99.99 });
}

// Response
// Content-Type: application/xml
// <Product><Name>Product</Name><Price>99.99</Price></Product>

Content Negotiation

[HttpGet]
public IActionResult Get()
{
    var product = new Product { Name = "Product", Price = 99.99 };
    return Ok(product);
}

// Request với Accept: application/json
// Response: JSON

// Request với Accept: application/xml
// Response: XML

JSON Configuration

System.Text.Json Options

builder.Services.AddControllers()
    .AddJsonOptions(options =>
    {
        // CamelCase property names
        options.JsonSerializerOptions.PropertyNamingPolicy = 
            JsonNamingPolicy.CamelCase;
        
        // Ignore null values
        options.JsonSerializerOptions.DefaultIgnoreCondition = 
            JsonIgnoreCondition.WhenWritingNull;
        
        // Write numbers as strings
        options.JsonSerializerOptions.NumberHandling = 
            JsonNumberHandling.WriteAsString;
        
        // Allow trailing commas
        options.JsonSerializerOptions.AllowTrailingCommas = true;
        
        // Custom converters
        options.JsonSerializerOptions.Converters.Add(
            new JsonStringEnumConverter());
    });

Newtonsoft.Json Options

builder.Services.AddControllers()
    .AddNewtonsoftJson(options =>
    {
        options.SerializerSettings.ContractResolver = 
            new CamelCasePropertyNamesContractResolver();
        
        options.SerializerSettings.NullValueHandling = 
            NullValueHandling.Ignore;
        
        options.SerializerSettings.DateFormatString = 
            "yyyy-MM-dd HH:mm:ss";
    });

Custom Output Formatter

public class CsvOutputFormatter : TextOutputFormatter
{
    public CsvOutputFormatter()
    {
        SupportedMediaTypes.Add("text/csv");
        SupportedEncodings.Add(Encoding.UTF8);
    }

    protected override bool CanWriteType(Type type)
    {
        return typeof(IEnumerable<Product>).IsAssignableFrom(type);
    }

    public override async Task WriteResponseBodyAsync(
        OutputFormatterWriteContext context,
        Encoding selectedEncoding)
    {
        var response = context.HttpContext.Response;
        var products = context.Object as IEnumerable<Product>;
        
        using var writer = new StreamWriter(response.Body, selectedEncoding);
        
        // Write header
        writer.WriteLine("Id,Name,Price");
        
        // Write data
        foreach (var product in products ?? Enumerable.Empty<Product>())
        {
            writer.WriteLine($"{product.Id},{product.Name},{product.Price}");
        }
    }
}

// Đăng ký
builder.Services.AddControllers(options =>
{
    options.OutputFormatters.Add(new CsvOutputFormatter());
});

Best Practices

1. Use JSON by Default

// ✅ JSON là default và được support tốt nhất
builder.Services.AddControllers();

// ❌ Tránh thêm quá nhiều formatters không cần thiết
builder.Services.AddControllers()
    .AddXmlSerializerFormatters()
    .AddXmlDataContractSerializerFormatters();

2. Configure Consistent JSON Settings

// ✅ Configure JSON settings globally
builder.Services.AddControllers()
    .AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.PropertyNamingPolicy = 
            JsonNamingPolicy.CamelCase;
        options.JsonSerializerOptions.WriteIndented = true;
    });

3. Use [Produces] Attribute

// ✅ Explicit về response format
[HttpGet]
[Produces("application/json")]
public IActionResult Get() => Ok(products);

// ❌ Không rõ response format
[HttpGet]
public IActionResult Get() => Ok(products);

Output Caching

Overview Questions

  • Output Caching là gì và khác Response Caching ra sao?
  • Làm sao để cấu hình output caching trong .NET 7+?
  • Cache policies hoạt động như thế nào?
  • Khi nào nên dùng output caching?
  • Cached tagged responses là gì?

Output Caching vs Response Caching

Comparison

┌─────────────────────────────────────────────────────────────────┐
│                    CACHING COMPARISON                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Output Caching (.NET 7+):                                      │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐                  │
│  │  Client  │───▶│  Server  │───▶│  Cache   │                  │
│  │  Request │    │  (App)   │    │  (Mem)   │                  │
│  └──────────┘    └──────────┘    └──────────┘                  │
│  - Cache trên server                                            │
│  - Không gửi cache headers cho client                          │
│  - Phù hợp cho server-side caching                             │
│                                                                 │
│  Response Caching:                                              │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐                  │
│  │  Client  │───▶│  Server  │───▶│  Client  │                  │
│  │  Request │    │  (App)   │    │  Cache   │                  │
│  └──────────┘    └──────────┘    └──────────┘                  │
│  - Cache trên client/proxy                                      │
│  - Gửi cache headers (Cache-Control)                           │
│  - Phù hợp cho CDN/browser caching                             │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Output Caching Configuration

Basic Setup

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Add output caching
builder.Services.AddOutputCache();

var app = builder.Build();

// Minimal APIs: Enable caching for endpoints
app.MapGet("/api/products", () => GetProducts())
    .CacheOutput();

app.Run();

MVC Controllers Setup

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Add output caching
builder.Services.AddOutputCache();

// Add controllers
builder.Services.AddControllers();

var app = builder.Build();

// IMPORTANT: Phải đặt sau UseRouting và trước MapControllers
app.UseRouting();
app.UseOutputCache();

app.MapControllers();

app.Run();

Cache Policies

builder.Services.AddOutputCache(options =>
{
    // Default policy - cache 60 seconds
    options.AddBasePolicy(builder => builder.Cache());
    
    // Custom policy - cache 5 minutes
    options.AddPolicy("ShortCache", builder => 
        builder.Cache().Expire(TimeSpan.FromMinutes(5)));
    
    // Custom policy - no cache
    options.AddPolicy("NoCache", builder => builder.NoCache());
    
    // Custom policy - vary by header
    options.AddPolicy("VaryByHeader", builder => 
        builder.Cache().VaryByHeader("Accept-Language"));
});

Endpoint Caching

Basic Caching (Minimal APIs)

// Cache với default policy
app.MapGet("/api/products", async (AppDbContext db) =>
{
    return await db.Products.ToListAsync();
})
.CacheOutput();

Basic Caching (MVC Controllers)

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet]
    [OutputCache(Duration = 60)]
    public async Task<IActionResult> GetProducts([FromServices] AppDbContext db)
    {
        return Ok(await db.Products.ToListAsync());
    }
}

Cache với custom policy (Controllers)

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet]
    [OutputCache(PolicyName = "ShortCache")]
    public async Task<IActionResult> GetProducts([FromServices] AppDbContext db)
    {
        return Ok(await db.Products.ToListAsync());
    }
}

### Vary By Query

```csharp
// Minimal APIs
app.MapGet("/api/products", async (int page, AppDbContext db) =>
{
    return await db.Products
        .Skip((page - 1) * 10)
        .Take(10)
        .ToListAsync();
})
.CacheOutput(b => b.VaryByQuery("page"));

// MVC Controllers
[HttpGet]
[OutputCache(Duration = 60, VaryByQueryKeys = new[] { "page", "category" })]
public async Task<IActionResult> GetProducts(
    [FromQuery] int page = 1, 
    [FromQuery] string category = null)
{
    var query = _db.Products.AsQueryable();
    if (!string.IsNullOrEmpty(category))
        query = query.Where(p => p.Category == category);
    
    return Ok(await query.Skip((page - 1) * 10).Take(10).ToListAsync());
}

Vary By Header

// Minimal APIs
app.MapGet("/api/products", async (AppDbContext db) =>
{
    return await db.Products.ToListAsync();
})
.CacheOutput(b => b.VaryByHeader("Accept-Language"));

// MVC Controllers
[HttpGet]
[OutputCache(Duration = 60, VaryByHeader = "Accept-Language")]
public async Task<IActionResult> GetProducts()
{
    return Ok(await _db.Products.ToListAsync());
}

Vary By Route

// Minimal APIs
app.MapGet("/api/products/{category}", async (string category, AppDbContext db) =>
{
    return await db.Products.Where(p => p.Category == category).ToListAsync();
})
.CacheOutput(b => b.VaryByRouteValue("category"));

// MVC Controllers
[HttpGet("{category}")]
[OutputCache(Duration = 60, VaryByRouteParameters = new[] { "category" })]
public async Task<IActionResult> GetProductsByCategory(string category)
{
    return Ok(await _db.Products.Where(p => p.Category == category).ToListAsync());
}

Cache Expiration

Time-based Expiration

// Minimal APIs
app.MapGet("/api/products", () => GetProducts())
    .CacheOutput(b => b.Expire(TimeSpan.FromMinutes(1)));

// MVC Controllers
[HttpGet]
[OutputCache(Duration = 60)] // 60 seconds
public IActionResult GetProducts() => Ok(_service.GetProducts());

// Hoặc với sliding expiration (thêm mới)
[HttpGet]
[OutputCache(Duration = 300, SlidingExpiration = true)]
public IActionResult GetProducts() => Ok(_service.GetProducts());

No Caching

// Minimal APIs
app.MapGet("/api/products", () => GetProducts())
    .CacheOutput(b => b.NoCache());

// MVC Controllers
[HttpGet]
[OutputCache(NoStore = true)]
public IActionResult GetProducts() => Ok(_service.GetProducts());

Cache Tags

Tag-based Invalidation

// Minimal APIs - Cache với tags
app.MapGet("/api/products", async (AppDbContext db) =>
{
    return await db.Products.ToListAsync();
})
.CacheOutput(b => b.Tag("products"));

app.MapGet("/api/products/{id}", async (int id, AppDbContext db) =>
{
    return await db.Products.FindAsync(id);
})
.CacheOutput(b => b.Tag($"product-{id}"));

// Invalidate cache khi update
app.MapPut("/api/products/{id}", async (int id, Product product, AppDbContext db, IOutputCacheStore cache) =>
{
    var existing = await db.Products.FindAsync(id);
    if (existing == null) return Results.NotFound();
    
    existing.Name = product.Name;
    existing.Price = product.Price;
    await db.SaveChangesAsync();
    
    // Evict cache
    await cache.EvictByTagAsync($"product-{id}", CancellationToken.None);
    
    return Results.NoContent();
});

Tag-based Invalidation (MVC Controllers)

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly AppDbContext _db;
    private readonly IOutputCacheStore _cache;

    public ProductsController(AppDbContext db, IOutputCacheStore cache)
    {
        _db = db;
        _cache = cache;
    }

    [HttpGet]
    [OutputCache(Duration = 300, Tags = new[] { "products" })]
    public async Task<IActionResult> GetProducts()
    {
        return Ok(await _db.Products.ToListAsync());
    }

    [HttpGet("{id}")]
    [OutputCache(Duration = 300, Tags = new[] { "product-{id}" })]
    public async Task<IActionResult> GetProduct(int id)
    {
        var product = await _db.Products.FindAsync(id);
        if (product == null) return NotFound();
        return Ok(product);
    }

    [HttpPut("{id}")]
    public async Task<IActionResult> UpdateProduct(int id, Product product)
    {
        var existing = await _db.Products.FindAsync(id);
        if (existing == null) return NotFound();
        
        existing.Name = product.Name;
        existing.Price = product.Price;
        await _db.SaveChangesAsync();
        
        // Invalidate cache by tag
        await _cache.EvictByTagAsync($"product-{id}", CancellationToken.None);
        await _cache.EvictByTagAsync("products", CancellationToken.None);
        
        return NoContent();
    }
}

---

## Response Caching (Client-side)

### Configuration

```csharp
// Add response caching middleware
builder.Services.AddResponseCaching();

var app = builder.Build();

app.UseResponseCaching();

// Minimal APIs
app.MapGet("/api/products", () => GetProducts());

// MVC Controllers
app.MapControllers();

Cache Headers (Minimal APIs)

app.MapGet("/api/products", () =>
{
    var response = Results.Ok(GetProducts());
    return response;
})
.AddEndpointFilter(async (context, next) =>
{
    var response = await next(context);
    
    context.HttpContext.Response.Headers["Cache-Control"] = 
        "public, max-age=60"; // Cache 60 seconds
    
    return response;
});

Cache Headers (MVC Controllers)

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet]
    [ResponseCache(Duration = 60, VaryByHeader = "Accept-Language")]
    public IActionResult GetProducts()
    {
        return Ok(_service.GetProducts());
    }
}

Best Practices

1. Cache Appropriately

// Minimal APIs
app.MapGet("/api/products", () => GetProducts())
    .CacheOutput(b => b.Expire(TimeSpan.FromMinutes(5)));

app.MapPost("/api/products", (Product p) => CreateProduct(p)); // Don't cache

// MVC Controllers
[HttpGet]
[OutputCache(Duration = 300)] // Cache read-heavy endpoints
public IActionResult GetProducts() => Ok(_service.GetProducts());

[HttpPost] // Don't cache write endpoints
public IActionResult CreateProduct(Product p) => Ok(_service.Create(p));

2. Use Tags for Invalidation

// Minimal APIs
app.MapGet("/api/products/{id}", (int id) => GetProduct(id))
    .CacheOutput(b => b.Tag($"product-{id}"));


// Invalidate when updated
await cache.EvictByTagAsync($"product-{id}", CancellationToken.None);

// MVC Controllers
[HttpGet("{id}")]
[OutputCache(Tags = new[] { "product-{id}" })]
public IActionResult GetProduct(int id) => Ok(_service.GetProduct(id));

3. Vary by Appropriate Keys

// Minimal APIs
app.MapGet("/api/products", (string? category, int page) => GetProducts(category, page))
    .CacheOutput(b => b.VaryByQuery("category", "page"));

// MVC Controllers
[HttpGet]
[OutputCache(VaryByQueryKeys = new[] { "category", "page" })]
public IActionResult GetProducts(
    [FromQuery] string category, 
    [FromQuery] int page) => Ok(_service.GetProducts(category, page));

Kestrel Configuration

Overview Questions

  • Kestrel là gì và tại sao nó quan trọng?
  • Làm sao để cấu hình HTTPS cho Kestrel?
  • Endpoint configuration hoạt động như thế nào?
  • Request limits được cấu hình ra sao?
  • Kestrel vs IIS/Nginx - khi nào dùng reverse proxy?

Kestrel Basics

What is Kestrel?

┌─────────────────────────────────────────────────────────────────┐
│                    KESTREL ARCHITECTURE                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐                  │
│  │  Client  │───▶│  Kestrel │───▶│  ASP.NET │                  │
│  │  (HTTP)  │    │  Server  │    │  Core    │                  │
│  └──────────┘    └──────────┘    └──────────┘                  │
│                                                                 │
│  - Cross-platform web server                                    │
│  - Default server cho ASP.NET Core                              │
│  - High performance                                             │
│  - Có thể dùng standalone hoặc với reverse proxy                │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Basic Configuration

// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    // Configure Kestrel here
});

var app = builder.Build();
app.Run();

Endpoint Configuration

appsettings.json

{
  "Kestrel": {
    "Endpoints": {
      "Http": {
        "Url": "http://localhost:5000"
      },
      "Https": {
        "Url": "https://localhost:5001"
      }
    }
  }
}

Code Configuration

builder.WebHost.ConfigureKestrel(options =>
{
    // HTTP endpoint
    options.ListenLocalhost(5000);
    
    // HTTPS endpoint
    options.ListenLocalhost(5001, listenOptions =>
    {
        listenOptions.UseHttps();
    });
    
    // Custom IP and port
    options.Listen(IPAddress.Parse("127.0.0.1"), 5002);
    
    // Unix socket (Linux)
    options.ListenUnixSocket("/tmp/kestrel.sock");
});

HTTPS Configuration

Development Certificate

builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenLocalhost(5001, listenOptions =>
    {
        listenOptions.UseHttps(); // Uses development certificate
    });
});

Production Certificate

builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenLocalhost(5001, listenOptions =>
    {
        listenOptions.UseHttps("certificate.pfx", "password");
    });
});

Certificate from Store

builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenLocalhost(5001, listenOptions =>
    {
        using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
        store.Open(OpenFlags.ReadOnly);
        
        var cert = store.Certificates
            .Find(X509FindType.FindByThumbprint, "CERT_THUMBPRINT", validOnly: false)
            .OfType<X509Certificate2>()
            .FirstOrDefault();
        
        if (cert != null)
        {
            listenOptions.UseHttps(cert);
        }
    });
});

HTTPS Redirection

// Program.cs
var app = builder.Build();

// Redirect HTTP to HTTPS
app.UseHttpsRedirection();

app.MapGet("/", () => "Hello HTTPS!");

app.Run();

Request Limits

Configure Limits

builder.WebHost.ConfigureKestrel(options =>
{
    // Max request body size (100MB)
    options.Limits.MaxRequestBodySize = 100 * 1024 * 1024;
    
    // Max request header size (32KB)
    options.Limits.MaxRequestHeadersTotalSize = 32 * 1024;
    
    // Max request header count
    options.Limits.MaxRequestHeaderCount = 100;
    
    // Keep-alive timeout
    options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2);
    
    // Request headers timeout
    options.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(30);
    
    // Min response data rate
    options.Limits.MinResponseDataRate = new MinDataRate(
        bytesPerSecond: 100, 
        gracePeriod: TimeSpan.FromSeconds(10));
});

Per-Request Limits

app.MapPost("/upload", async (HttpContext context) =>
{
    // Override limit for this request
    context.Features.Get<IHttpMaxRequestBodySizeFeature>()
        ?.MaxRequestBodySize = 200 * 1024 * 1024; // 200MB
    
    // Process upload
    await context.Request.Body.CopyToAsync(Stream.Null);
    
    return Results.Ok();
});

HTTP/2 and HTTP/3

HTTP/2 Configuration

builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenLocalhost(5001, listenOptions =>
    {
        listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
        listenOptions.UseHttps();
    });
});

HTTP/3 Configuration

builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenLocalhost(5002, listenOptions =>
    {
        listenOptions.Protocols = HttpProtocols.Http3;
    });
});

Protocol Selection

// Check protocol
app.MapGet("/protocol", (HttpContext context) =>
{
    var protocol = context.Request.Protocol;
    return new { Protocol = protocol };
});

Reverse Proxy

When to Use Reverse Proxy

┌─────────────────────────────────────────────────────────────────┐
│                    REVERSE PROXY SETUP                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐                  │
│  │  Client  │───▶│  Nginx/  │───▶│  Kestrel │                  │
│  │          │    │  IIS     │    │  (App)   │                  │
│  └──────────┘    └──────────┘    └──────────┘                  │
│                                                                 │
│  Use reverse proxy for:                                         │
│  - Multiple apps trên cùng port 80/443                         │
│  - Static file serving                                          │
│  - Response compression                                         │
│  - Additional security layer                                    │
│  - Load balancing                                               │
│                                                                 │
│  Kestrel standalone OK cho:                                     │
│  - Internal services                                            │
│  - Container deployments                                        │
│  - Development                                                  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Forwarded Headers

// Khi dùng reverse proxy
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor | 
                       ForwardedHeaders.XForwardedProto
});

Best Practices

1. Use HTTPS in Production

// ✅ Always use HTTPS in production
builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenLocalhost(5001, o => o.UseHttps("cert.pfx", "password"));
});

// ❌ Never use HTTP only in production
builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenLocalhost(5000); // No HTTPS!
});

2. Set Appropriate Limits

// ✅ Set reasonable limits
options.Limits.MaxRequestBodySize = 50 * 1024 * 1024; // 50MB

// ❌ Don't remove limits entirely
options.Limits.MaxRequestBodySize = null; // Unlimited!

3. Configure for Environment

if (builder.Environment.IsDevelopment())
{
    builder.WebHost.ConfigureKestrel(options =>
    {
        options.ListenLocalhost(5000);
        options.ListenLocalhost(5001, o => o.UseHttps());
    });
}
else
{
    builder.WebHost.ConfigureKestrel(options =>
    {
        options.ListenLocalhost(5001, o => 
            o.UseHttps("prod-cert.pfx", config["CertPassword"]));
    });
}

Localization (i18n)

Overview Questions

  • Localization trong ASP.NET Core hoạt động như thế nào?
  • Resource files là gì và cách sử dụng?
  • Làm sao để localize controllers và views?
  • Culture providers hoạt động ra sao?
  • Localize data annotations và validation messages?

Localization Setup

Configuration

// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddLocalization(options =>
{
    options.ResourcesPath = "Resources";
});

builder.Services.AddControllersWithViews()
    .AddViewLocalization(LanguageViewLocationFormatSuffixFormat.Suffix)
    .AddDataAnnotationsLocalization();

var app = builder.Build();

// Supported cultures
var supportedCultures = new[] { "en-US", "vi-VN", "fr-FR" };
var localizationOptions = new RequestLocalizationOptions()
    .SetDefaultCulture(supportedCultures[0])
    .AddSupportedCultures(supportedCultures)
    .AddSupportedUICultures(supportedCultures);

app.UseRequestLocalization(localizationOptions);

app.Run();

Resource Files

File Structure

Project/
├── Resources/
│   ├── Controllers/
│   │   ├── HomeController.en-US.resx
│   │   ├── HomeController.vi-VN.resx
│   │   └── HomeController.fr-FR.resx
│   ├── Views/
│   │   ├── Home/
│   │   │   ├── Index.en-US.resx
│   │   │   └── Index.vi-VN.resx
│   │   └── Shared/
│   │       └── _Layout.en-US.resx
│   ├── SharedResource.en-US.resx
│   └── SharedResource.vi-VN.resx
└── Program.cs

Resource File Content

<!-- SharedResource.en-US.resx -->
<data name="Hello" xml:space="preserve">
  <value>Hello</value>
</data>
<data name="Welcome" xml:space="preserve">
  <value>Welcome to our application</value>
</data>

<!-- SharedResource.vi-VN.resx -->
<data name="Hello" xml:space="preserve">
  <value>Xin chào</value>
</data>
<data name="Welcome" xml:space="preserve">
  <value>Chào mừng bạn đến với ứng dụng</value>
</data>

Using IStringLocalizer

Basic Usage

public class HomeController : Controller
{
    private readonly IStringLocalizer<SharedResource> _localizer;

    public HomeController(IStringLocalizer<SharedResource> localizer)
    {
        _localizer = localizer;
    }

    public IActionResult Index()
    {
        ViewData["Message"] = _localizer["Hello"];
        ViewData["Welcome"] = _localizer["Welcome"];
        return View();
    }
}

With Parameters

// Resource file
// "Greeting" = "Hello {0}, welcome to {1}"

public IActionResult Index(string name, string city)
{
    var message = _localizer["Greeting", name, city];
    // Output: "Hello John, welcome to New York"
    return View();
}

IStringLocalizer vs IHtmlLocalizer

// IStringLocalizer - plain text
public class MyController : Controller
{
    private readonly IStringLocalizer<MyController> _localizer;

    public MyController(IStringLocalizer<MyController> localizer)
    {
        _localizer = localizer;
    }
}

// IHtmlLocalizer - HTML content (không encode HTML)
public class MyViewComponent : ViewComponent
{
    private readonly IHtmlLocalizer<SharedResource> _localizer;

    public MyViewComponent(IHtmlLocalizer<SharedResource> localizer)
    {
        _localizer = localizer;
    }

    public IViewComponentResult Invoke()
    {
        // Resource: "Bold" = "<strong>Bold text</strong>"
        var html = _localizer["Bold"]; // Không encode HTML
        return View(html);
    }
}

View Localization

In Razor Views

@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer Localizer

<h1>@Localizer["Welcome"]</h1>
<p>@Localizer["Hello"]</p>

<!-- Hoặc dùng IHtmlLocalizer -->
@inject IHtmlLocalizer<SharedResource> HtmlLocalizer

<p>@HtmlLocalizer["Description"]</p>

View with Model

@model Product
@inject IViewLocalizer Localizer

<h1>@Localizer["ProductDetails"]</h1>
<div>
    <label>@Localizer["Name"]</label>
    <span>@Model.Name</span>
</div>
<div>
    <label>@Localizer["Price"]</label>
    <span>@Model.Price</span>
</div>

Data Annotations Localization

Localized Validation Messages

public class Product
{
    [Required(ErrorMessage = "NameRequired")]
    [Display(Name = "ProductName")]
    public string Name { get; set; } = string.Empty;

    [Range(0.01, 9999.99, ErrorMessage = "PriceRange")]
    [Display(Name = "ProductPrice")]
    public decimal Price { get; set; }
}

// Resource file
// "NameRequired" = "Tên sản phẩm là bắt buộc"
// "ProductName" = "Tên sản phẩm"
// "PriceRange" = "Giá phải từ 0.01 đến 9999.99"
// "ProductPrice" = "Giá sản phẩm"

Configuration

builder.Services.AddControllersWithViews()
    .AddViewLocalization()
    .AddDataAnnotationsLocalization(options =>
    {
        options.DataAnnotationLocalizerProvider = 
            (type, factory) => factory.Create(typeof(SharedResource));
    });

Culture Providers

Request Culture Providers

┌─────────────────────────────────────────────────────────────────┐
│                  CULTURE PROVIDERS                               │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Thứ tự kiểm tra (theo mặc định):                               │
│  1. QueryStringRequestCultureProvider                           │
│     ?culture=vi-VN&ui-culture=vi-VN                            │
│                                                                 │
│  2. CookieRequestCultureProvider                                │
│     Cookie: .AspNetCore.Culture=c=vi-VN\|uic=vi-VN             │
│                                                                 │
│  3. AcceptLanguageHeaderRequestCultureProvider                  │
│     Accept-Language: vi-VN, en-US;q=0.9                        │
│                                                                 │
│  Custom Provider:                                               │
│  - Route data (/vi/products, /en/products)                      │
│  - Subdomain (vi.example.com, en.example.com)                   │
│  - Custom header (X-Culture: vi-VN)                             │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Custom Culture Provider

public class RouteDataRequestCultureProvider : RequestCultureProvider
{
    public int RouteDataIndex = 0;

    public override Task<ProviderCultureResult> DetermineProviderCultureResult(
        HttpContext httpContext)
    {
        var culture = httpContext.Request.RouteValues["culture"]?.ToString();
        
        if (string.IsNullOrEmpty(culture))
        {
            return NullProviderCultureResult;
        }

        var result = new ProviderCultureResult(culture);
        return Task.FromResult(result);
    }
}

// Đăng ký
var localizationOptions = new RequestLocalizationOptions()
    .SetDefaultCulture("en-US")
    .AddSupportedCultures("en-US", "vi-VN");

// Thêm custom provider vào đầu danh sách
localizationOptions.RequestCultureProviders.Insert(
    0, new RouteDataRequestCultureProvider());

app.UseRequestLocalization(localizationOptions);
// Set culture cookie
app.MapGet("/set-culture/{culture}", (string culture, HttpContext context) =>
{
    context.Response.Cookies.Append(
        CookieRequestCultureProvider.DefaultCookieName,
        CookieRequestCultureProvider.MakeCookieValue(
            new RequestCulture(culture)));
    
    return Results.Redirect("/");
});

Best Practices

1. Use Shared Resources

// ✅ Good - centralized resources
public class SharedResource { } // Empty class

// Inject shared localizer
public class HomeController : Controller
{
    private readonly IStringLocalizer<SharedResource> _localizer;
    
    public HomeController(IStringLocalizer<SharedResource> localizer)
    {
        _localizer = localizer;
    }
}

2. Fallback Culture

var localizationOptions = new RequestLocalizationOptions()
    .SetDefaultCulture("en-US")
    .AddSupportedCultures("en-US", "vi-VN")
    .AddSupportedUICultures("en-US", "vi-VN")
    .SetFallbackCulture("en-US"); // Fallback nếu không tìm thấy culture

3. Culture in URLs

// Route với culture
app.MapControllerRoute(
    name: "localized",
    pattern: "{culture=en-US}/{controller=Home}/{action=Index}");

// URLs:
// /en-US/Home/Index
// /vi-VN/Home/Index

SignalR

Overview Questions

  • SignalR là gì và khi nào cần sử dụng?
  • Hub là gì và cách tạo Hub?
  • Client-server communication hoạt động ra sao?
  • Groups và Users trong SignalR khác nhau như thế nào?
  • Scale SignalR với Redis backplane ra sao?

SignalR Basics

What is SignalR?

┌─────────────────────────────────────────────────────────────────┐
│                    SIGNALR ARCHITECTURE                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐                  │
│  │  Client  │◀──▶│   Hub    │◀──▶│  Client  │                  │
│  │  (Web)   │    │  (Server)│    │  (Web)   │                  │
│  └──────────┘    └──────────┘    └──────────┘                  │
│       │                                │                        │
│       │         ┌──────────┐           │                        │
│       └────────▶│  Client  │◀──────────┘                        │
│                 │  (Mobile)│                                     │
│                 └──────────┘                                     │
│                                                                 │
│  - Real-time communication                                      │
│  - Server push to clients                                       │
│  - WebSocket, Server-Sent Events, Long Polling                  │
│  - Chat, notifications, live updates                            │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Setup

dotnet add package Microsoft.AspNetCore.SignalR
// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSignalR();

var app = builder.Build();

app.MapHub<ChatHub>("/chat");

app.Run();

Hub

Basic Hub

public class ChatHub : Hub
{
    // Client gọi method này
    public async Task SendMessage(string user, string message)
    {
        // Server gọi method trên tất cả clients
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }

    // Gửi cho người gọi
    public async Task SendMessageToCaller(string user, string message)
    {
        await Clients.Caller.SendAsync("ReceiveMessage", user, message);
    }

    // Gửi cho người khác (trừ người gọi)
    public async Task SendMessageToOthers(string user, string message)
    {
        await Clients.Others.SendAsync("ReceiveMessage", user, message);
    }
}

Hub with Dependency Injection

public class ChatHub : Hub
{
    private readonly ILogger<ChatHub> _logger;
    private readonly IChatService _chatService;

    public ChatHub(ILogger<ChatHub> logger, IChatService chatService)
    {
        _logger = logger;
        _chatService = chatService;
    }

    public async Task SendMessage(string user, string message)
    {
        _logger.LogInformation("Message from {User}: {Message}", user, message);
        
        // Save to database
        await _chatService.SaveMessage(user, message);
        
        // Broadcast
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
}

Client Communication

Server to Client

public class NotificationHub : Hub
{
    // Gửi cho tất cả
    public async Task Broadcast(string message)
    {
        await Clients.All.SendAsync("OnBroadcast", message);
    }

    // Gửi cho client cụ thể (theo ConnectionId)
    public async Task SendToClient(string connectionId, string message)
    {
        await Clients.Client(connectionId).SendAsync("OnMessage", message);
    }

    // Gửi cho nhóm
    public async Task SendToGroup(string group, string message)
    {
        await Clients.Group(group).SendAsync("OnMessage", message);
    }

    // Gửi cho user cụ thể (theo UserId)
    public async Task SendToUser(string userId, string message)
    {
        await Clients.User(userId).SendAsync("OnMessage", message);
    }
}

Client to Server

// JavaScript Client
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chat")
    .build();

// Receive message from server
connection.on("ReceiveMessage", (user, message) => {
    console.log(`${user}: ${message}`);
});

// Send message to server
connection.invoke("SendMessage", "John", "Hello!").catch(err => console.error(err));

// Start connection
connection.start().catch(err => console.error(err));

Groups

Group Management

public class ChatHub : Hub
{
    // Join group
    public async Task JoinGroup(string groupName)
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
        
        await Clients.Group(groupName).SendAsync(
            "OnUserJoined", 
            $"{Context.ConnectionId} joined {groupName}");
    }

    // Leave group
    public async Task LeaveGroup(string groupName)
    {
        await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
        
        await Clients.Group(groupName).SendAsync(
            "OnUserLeft", 
            $"{Context.ConnectionId} left {groupName}");
    }

    // Send to group
    public async Task SendToGroup(string groupName, string message)
    {
        await Clients.Group(groupName).SendAsync("OnMessage", message);
    }
}

Group Lifecycle

public class ChatHub : Hub
{
    public override async Task OnConnectedAsync()
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, "General");
        await Clients.Caller.SendAsync("OnConnected", Context.ConnectionId);
        await base.OnConnectedAsync();
    }

    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        await Groups.RemoveFromGroupAsync(Context.ConnectionId, "General");
        await Clients.Others.SendAsync(
            "OnDisconnected", 
            Context.ConnectionId);
        await base.OnDisconnectedAsync(exception);
    }
}

Strongly-typed Hubs

Interface-based Hub

// Define client methods interface
public interface IChatClient
{
    Task ReceiveMessage(string user, string message);
    Task UserJoined(string user);
    Task UserLeft(string user);
}

// Strongly-typed Hub
public class ChatHub : Hub<IChatClient>
{
    public async Task SendMessage(string user, string message)
    {
        // Type-safe client calls
        await Clients.All.ReceiveMessage(user, message);
    }

    public async Task JoinGroup(string groupName)
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
        await Clients.Group(groupName).UserJoined(Context.ConnectionId);
    }
}

Authentication & Authorization

Hub Authorization

// Program.cs
app.MapHub<ChatHub>("/chat", options =>
{
    options.Transports = HttpTransportType.WebSockets | 
                         HttpTransportType.ServerSentEvents;
});

// Hub với authorization
[Authorize]
public class ChatHub : Hub
{
    public override async Task OnConnectedAsync()
    {
        var userId = Context.UserIdentifier; // From JWT claim
        var userName = Context.User?.Identity?.Name;
        
        await base.OnConnectedAsync();
    }
}

Scaling

Redis Backplane

dotnet add package Microsoft.AspNetCore.SignalR.StackExchangeRedis
builder.Services.AddSignalR()
    .AddStackExchangeRedis("localhost:6379");

Azure SignalR Service

dotnet add package Microsoft.Azure.SignalR
builder.Services.AddSignalR()
    .AddAzureSignalR(options =>
    {
        options.ConnectionString = 
            builder.Configuration["Azure:SignalR:ConnectionString"];
    });

// Map hub
app.MapHub<ChatHub>("/chat");

Best Practices

1. Handle Connection Lifecycle

// ✅ Good - handle connection events
public class ChatHub : Hub
{
    public override async Task OnConnectedAsync()
    {
        _logger.LogInformation("Client connected: {ConnectionId}", Context.ConnectionId);
        await base.OnConnectedAsync();
    }

    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        _logger.LogInformation("Client disconnected: {ConnectionId}", Context.ConnectionId);
        await base.OnDisconnectedAsync(exception);
    }
}

2. Use Groups for Targeted Messages

// ✅ Good - use groups for targeted messages
public async Task SendToRoom(string roomId, string message)
{
    await Clients.Group($"room-{roomId}").SendAsync("OnMessage", message);
}

// ❌ Bad - send to all when only room needs it
public async Task SendToRoom(string roomId, string message)
{
    await Clients.All.SendAsync("OnMessage", message); // All clients receive!
}

3. Handle Reconnection

// JavaScript Client - handle reconnection
connection.onclose(async () => {
    await start();
});

async function start() {
    try {
        await connection.start();
        console.log("SignalR connected");
    } catch (err) {
        console.log("SignalR connection failed");
        setTimeout(() => start(), 5000);
    }
}

gRPC in ASP.NET Core

Overview Questions

  • gRPC là gì và khác REST API như thế nào?
  • Protocol Buffers (protobuf) là gì?
  • Làm sao để tạo gRPC service trong ASP.NET Core?
  • Unary, Server Streaming, Client Streaming, Bidirectional Streaming là gì?
  • Khi nào nên dùng gRPC thay vì REST?

gRPC Basics

What is gRPC?

┌─────────────────────────────────────────────────────────────────┐
│                    gRPC vs REST                                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  REST:                                                          │
│  ┌──────────┐    HTTP/JSON    ┌──────────┐                     │
│  │  Client  │◀───────────────▶│  Server  │                     │
│  └──────────┘                 └──────────┘                     │
│  - Text-based (JSON)                                              │
│  - Human readable                                                 │
│  - Larger payload size                                            │
│  - Request/Response only                                          │
│                                                                 │
│  gRPC:                                                          │
│  ┌──────────┐   HTTP/2/Proto  ┌──────────┐                     │
│  │  Client  │◀───────────────▶│  Server  │                     │
│  └──────────┘   buf           └──────────┘                     │
│  - Binary (Protocol Buffers)                                    │
│  - Not human readable                                           │
│  - Smaller payload size                                         │
│  - Streaming support                                            │
│  - Strongly typed contracts                                     │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

When to Use gRPC

┌─────────────────────────────────────────────────────────────────┐
│                    WHEN TO USE gRPC                               │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Use gRPC for:                                                  │
│  - Microservices communication                                  │
│  - Real-time streaming                                          │
│  - Low latency requirements                                     │
│  - Polyglot environments (multiple languages)                   │
│  - Internal service-to-service calls                            │
│                                                                 │
│  Use REST for:                                                  │
│  - Public APIs                                                  │
│  - Browser clients                                              │
│  - Simple CRUD operations                                       │
│  - When human readability matters                               │
│  - When caching is important                                    │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Protocol Buffers

.proto File

// Protos/greet.proto
syntax = "proto3";

option csharp_namespace = "MyGrpcService";

package greet;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply);
  
  // Streaming: server sends multiple responses
  rpc SayHelloStream (HelloRequest) returns (stream HelloReply);
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greeting.
message HelloReply {
  string message = 1;
}

Project Configuration

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Grpc.AspNetCore" Version="2.57.0" />
  </ItemGroup>
</Project>

gRPC Service

Basic Service

public class GreeterService : Greeter.GreeterBase
{
    private readonly ILogger<GreeterService> _logger;

    public GreeterService(ILogger<GreeterService> logger)
    {
        _logger = logger;
    }

    public override Task<HelloReply> SayHello(
        HelloRequest request, 
        ServerCallContext context)
    {
        _logger.LogInformation("Saying hello to {Name}", request.Name);
        
        return Task.FromResult(new HelloReply
        {
            Message = $"Hello {request.Name}"
        });
    }
}

Server Configuration

// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddGrpc();

var app = builder.Build();

app.MapGrpcService<GreeterService>();

app.Run();

Streaming

Server Streaming

service ChatService {
  // Server sends multiple messages
  rpc Subscribe (SubscribeRequest) returns (stream ChatMessage);
}

message SubscribeRequest {
  string channel = 1;
}

message ChatMessage {
  string user = 1;
  string message = 2;
  int64 timestamp = 3;
}
public class ChatService : ChatService.ChatServiceBase
{
    private readonly IChannelReader<ChatMessage> _channel;

    public ChatService(IChannelReader<ChatMessage> channel)
    {
        _channel = channel;
    }

    public override async Task Subscribe(
        SubscribeRequest request,
        IServerStreamWriter<ChatMessage> responseStream,
        ServerCallContext context)
    {
        await foreach (var message in _channel.ReadAllAsync(context.CancellationToken))
        {
            if (message.Channel == request.Channel)
            {
                await responseStream.WriteAsync(message);
            }
        }
    }
}

Client Streaming

service UploadService {
  // Client sends multiple messages
  rpc Upload (stream FileChunk) returns (UploadResponse);
}

message FileChunk {
  bytes data = 1;
  int32 chunk_number = 2;
}

message UploadResponse {
  string file_id = 1;
  int64 total_size = 2;
}
public class UploadService : UploadService.UploadServiceBase
{
    public override async Task<UploadResponse> Upload(
        IAsyncStreamReader<FileChunk> requestStream,
        ServerCallContext context)
    {
        var totalSize = 0L;
        var fileId = Guid.NewGuid().ToString();
        
        await foreach (var chunk in requestStream.ReadAllAsync())
        {
            totalSize += chunk.Data.Length;
            // Process chunk
        }
        
        return new UploadResponse
        {
            FileId = fileId,
            TotalSize = totalSize
        };
    }
}

Bidirectional Streaming

service ChatService {
  // Both client and server stream
  rpc Chat (stream ChatMessage) returns (stream ChatMessage);
}
public class ChatService : ChatService.ChatServiceBase
{
    public override async Task Chat(
        IAsyncStreamReader<ChatMessage> requestStream,
        IServerStreamWriter<ChatMessage> responseStream,
        ServerCallContext context)
    {
        // Read from client and broadcast to all
        await foreach (var message in requestStream.ReadAllAsync())
        {
            // Process and broadcast
            await responseStream.WriteAsync(new ChatMessage
            {
                User = "Server",
                Message = $"Echo: {message.Message}"
            });
        }
    }
}

gRPC Client

Setup Client

dotnet add package Grpc.Net.Client
dotnet add package Google.Protobuf
dotnet add package Grpc.Tools

Client Code

// Program.cs
using var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new Greeter.GreeterClient(channel);

// Unary call
var response = await client.SayHelloAsync(
    new HelloRequest { Name = "World" });

Console.WriteLine(response.Message);

gRPC Client Factory

builder.Services.AddGrpcClient<Greeter.GreeterClient>(options =>
{
    options.Address = new Uri("https://localhost:5001");
});

// Usage
public class MyService
{
    private readonly Greeter.GreeterClient _client;

    public MyService(Greeter.GreeterClient client)
    {
        _client = client;
    }

    public async Task<string> Greet(string name)
    {
        var response = await _client.SayHelloAsync(
            new HelloRequest { Name = name });
        return response.Message;
    }
}

Interceptors

Server Interceptor

public class LoggingInterceptor : Interceptor
{
    private readonly ILogger<LoggingInterceptor> _logger;

    public LoggingInterceptor(ILogger<LoggingInterceptor> logger)
    {
        _logger = logger;
    }

    public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
        TRequest request,
        ServerCallContext context,
        UnaryServerMethod<TRequest, TResponse> continuation)
    {
        _logger.LogInformation("Starting call {Method}", context.Method);
        
        try
        {
            return await continuation(request, context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error in call {Method}", context.Method);
            throw;
        }
    }
}

// Register
builder.Services.AddGrpc(options =>
{
    options.Interceptors.Add<LoggingInterceptor>();
});

Best Practices

1. Use Appropriate Call Types

// ✅ Unary for simple request/response
var response = await client.SayHelloAsync(request);

// ✅ Server streaming for real-time updates
await foreach (var message in client.SubscribeAsync(request))
{
    Console.WriteLine(message.Message);
}

// ✅ Client streaming for uploads
var uploadResponse = await client.UploadAsync(requestStream);

// ✅ Bidirectional for chat
await foreach (var msg in client.ChatAsync(requestStream))
{
    // Process
}

2. Handle Errors Properly

try
{
    var response = await client.SayHelloAsync(request);
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.NotFound)
{
    Console.WriteLine("Resource not found");
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.Unavailable)
{
    Console.WriteLine("Service unavailable");
}
catch (RpcException ex)
{
    Console.WriteLine($"gRPC error: {ex.StatusCode}");
}

3. Use Deadlines

// Set deadline for call
var deadline = DateTime.UtcNow.AddSeconds(5);
var response = await client.SayHelloAsync(
    request,
    deadline: deadline);

3. Xây dựng Web API

Giới thiệu

Phần này trình bày cách xây dựng RESTful API với ASP.NET Core.

Nội dung chính

RESTful API

Bảo mật

Phiên bản & Tài liệu

Tối ưu hiệu suất

RESTful API

Overview Questions

  • REST là gì và tại sao nó quan trọng?
  • Các HTTP methods nào được sử dụng và khi nào dùng?
  • Status codes nào nên dùng cho từng trường hợp?
  • URL naming conventions như thế nào cho đúng chuẩn?
  • Paging, filtering, sorting được implement ra sao?

Thiết kế API đúng chuẩn REST

REST Principles

  1. Client-Server - Tách biệt client và server
  2. Stateless - Mỗi request chứa đủ thông tin
  3. Cacheable - Response có thể được cache
  4. Uniform Interface - Sử dụng HTTP methods và status codes đúng cách
  5. Layered System - Có thể có nhiều layers

HTTP Methods

MethodPurposeIdempotent
GETLấy resource✅ Yes
POSTTạo resource mới❌ No
PUTThay thế toàn bộ resource✅ Yes
PATCHCập nhật một phần resource❌ No
DELETEXóa resource✅ Yes

Status Codes

CodeMeaningUsage
200OKGET, PUT, PATCH thành công
201CreatedPOST tạo mới thành công
204No ContentDELETE thành công
400Bad RequestValidation failed
401UnauthorizedChưa authenticate
403ForbiddenKhông có permission
404Not FoundResource không tồn tại
500Internal Server ErrorServer error

URL Naming Conventions

// ✅ Tốt
GET    /api/products              // Lấy danh sách products
GET    /api/products/1          // Lấy product có id = 1
POST   /api/products             // Tạo product mới
PUT    /api/products/1          // Cập nhật product 1
DELETE /api/products/1          // Xóa product 1

// ❌ Tránh
GET    /api/getProducts
GET    /api/ProductController/GetAll
POST   /api/createProduct

Model Binding & Validation

Note: Model Binding và Model Validation đã được chuyển sang các file riêng để dễ học hơn:

Quick Reference

[ApiController]
public class ProductsController : ControllerBase
{
    // FromRoute - Từ URL path
    [HttpGet("{id}")]
    public IActionResult GetById([FromRoute] int id)
    {
        return Ok(id);
    }
    
    // FromQuery - Từ query string
    [HttpGet]
    public IActionResult Search([FromQuery] string name, [FromQuery] int page = 1)
    {
        return Ok(new { name, page });
    }
    
    // FromBody - Từ request body (JSON)
    [HttpPost]
    public IActionResult Create([FromBody] Product product)
    {
        // [ApiController] tự động validate ModelState
        return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
    }
}

Paging, Filtering, Sorting

[HttpGet]
public IActionResult GetProducts(
    [FromQuery] PagingParameters paging,
    [FromQuery] ProductFilter filter,
    [FromQuery] string sortBy = "Name")
{
    var query = _context.Products.AsQueryable();
    
    // Filtering
    if (!string.IsNullOrEmpty(filter.Category))
        query = query.Where(p => p.Category == filter.Category);
    
    if (filter.MinPrice.HasValue)
        query = query.Where(p => p.Price >= filter.MinPrice.Value);
    
    // Sorting
    query = sortBy?.ToLower() switch
    {
        "price" => query.OrderBy(p => p.Price),
        "pricedesc" => query.OrderByDescending(p => p.Price),
        _ => query.OrderBy(p => p.Name)
    };
    
    // Paging
    var total = query.Count();
    var items = query
        .Skip((paging.Page - 1) * paging.PageSize)
        .Take(paging.PageSize)
        .ToList();
    
    return Ok(new PagedResult(items, total, paging.Page, paging.PageSize));
}

public class PagingParameters
{
    [FromQuery(Name = "page")]
    public int Page { get; set; } = 1;
    
    [FromQuery(Name = "pageSize")]
    public int PageSize { get; set; } = 10;
}

public class ProductFilter
{
    [FromQuery(Name = "category")]
    public string Category { get; set; }
    
    [FromQuery(Name = "minPrice")]
    public decimal? MinPrice { get; set; }
    
    [FromQuery(Name = "maxPrice")]
    public decimal? MaxPrice { get; set; }
}

Bảo mật

Authentication (Xác thực)

JWT (JSON Web Token)

JWT là một chuẩn token để truyền thông tin an toàn giữa các parties dưới dạng JSON.

Cấu trúc JWT

┌─────────────────────────────────────────────────────────────────────┐
│                        JWT STRUCTURE                                │
├─────────────────┬─────────────────┬───────────────────────────────┤
│    HEADER       │     PAYLOAD     │         SIGNATURE             │
│  (Base64URL)    │   (Base64URL)   │       (Base64URL)             │
├─────────────────┼─────────────────┼───────────────────────────────┤
│ {               │ {               │ HmacSHA256(                   │
│   "alg":        │   "sub":        │   header + "." + payload,     │
│     "HS256",    │     "1234567890",│   secret_key                  │
│   "typ":        │   "name":       │ )                             │
│     "JWT"       │     "John Doe", │                               │
│ }               │   "iat":         │                               │
│                 │     1516239022,  │                               │
│                 │   "exp":         │                               │
│                 │     1516242622   │                               │
│                 │ }               │                               │
└─────────────────┴─────────────────┴───────────────────────────────┘

Cấu hình JwtBearer trong .NET Core

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Add Authentication
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidIssuer = builder.Configuration["Jwt:Issuer"],
        ValidAudience = builder.Configuration["Jwt:Audience"],
        IssuerSigningKey = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(builder.Configuration["Jwt:SecretKey"]))
    };
});

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

Tạo JWT Token

public class JwtService
{
    private readonly JwtSettings _settings;
    
    public string GenerateToken(User user)
    {
        var claims = new[]
        {
            new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
            new Claim(JwtRegisteredClaimNames.Email, user.Email),
            new Claim(ClaimTypes.Role, user.Role),
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
        };
        
        var key = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(_settings.SecretKey));
        var credentials = new SigningCredentials(
            key, SecurityAlgorithms.HmacSha256);
        
        var token = new JwtSecurityToken(
            issuer: _settings.Issuer,
            audience: _settings.Audience,
            claims: claims,
            expires: DateTime.UtcNow.AddMinutes(_settings.ExpiryMinutes),
            signingCredentials: credentials);
        
        return new JwtSecurityTokenHandler().WriteToken(token);
    }
}

Authorization (Phân quyền)

[Authorize] Attribute

// Yêu cầu authenticate
[Authorize]
[HttpGet("profile")]
public IActionResult GetProfile() { }

// Yêu cầu role cụ thể
[Authorize(Roles = "Admin")]
[HttpGet("admin")]
public IActionResult GetAdminData() { }

// Yêu cầu nhiều roles (AND logic)
[Authorize(Roles = "Admin,Manager")]
[HttpGet("manage")]
public IActionResult Manage() { }

// Yếu tố OR - dùng Policy
[Authorize(Policy = "AdminOrManager")]
[HttpGet("manage")]
public IActionResult Manage() { }

Role-based Authorization

[Authorize(Roles = "Admin")]
public class AdminController : ControllerBase
{
    [HttpGet("users")]
    public IActionResult GetAllUsers()
    {
        // Only admins can access
        return Ok();
    }
}

Policy-based Authorization

// Đăng ký policy trong Program.cs
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdultOnly", policy =>
        policy.RequireClaim("Age", "18", "19", "20", "21", "22", "23", "24", "25"));
    
    options.AddPolicy("PremiumUser", policy =>
        policy.RequireAssertion(context =>
            context.User.HasClaim(c => c.Type == "Subscription" && 
                                      c.Value == "Premium")));
    
    options.AddPolicy("CanDeleteProduct", policy =>
        policy.RequireAssertion(context =>
            context.User.IsInRole("Admin") ||
            (context.User.IsInRole("Manager") && 
             context.User.HasClaim(c => c.Type == "CanDelete"))));
});

// Sử dụng
[Authorize(Policy = "PremiumUser")]
[HttpGet("premium-content")]
public IActionResult GetPremiumContent() { }

CORS (Cross-Origin Resource Sharing)

Vấn đề

Browser chặn requests từ một domain khác với server (cross-origin requests) vì lý do bảo mật. CORS cho phép server chỉ định origins nào được phép truy cập.

Cấu hình CORS

// Program.cs
builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowFrontend", policy =>
    {
        policy.WithOrigins("http://localhost:3000", "https://myapp.com")
              .AllowAnyHeader()
              .AllowAnyMethod()
              .AllowCredentials(); // Chỉ dùng với specific origins
    });
    
    options.AddPolicy("AllowAll", policy =>
    {
        policy.AllowAnyOrigin()
              .AllowAnyHeader()
              .AllowAnyMethod();
    });
});

var app = builder.Build();

app.UseCors("AllowFrontend");

CORS với named policy

[ApiController]
[Route("api/[controller]")]
[EnableCors("AllowFrontend")]
public class ProductsController : ControllerBase
{
    [HttpGet]
    public IActionResult Get() => Ok();
    
    [HttpGet]
    [DisableCors] // Disable CORS cho action cụ thể
    public IActionResult GetSecret() => Ok();
}

Preflight Request

┌─────────────────────────────────────────────────────────────────────┐
│                     CORS REQUEST FLOW                              │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  Browser                                                        │
│      │                                                              │
│      │ 1. OPTIONS /api/products                                  │
│      │    Access-Control-Request-Method: GET                     │
│      │    Access-Control-Request-Headers: Content-Type          │
│      ├────────────────────────────────────────────────────────►   │
│      │◄────────────────────────────────────────────────────────┤  │
│      │ 2. 200 OK                                                 │
│      │    Access-Control-Allow-Origin: http://localhost:3000   │
│      │    Access-Control-Allow-Methods: GET, POST, PUT, DELETE │
│      │    Access-Control-Allow-Headers: Content-Type            │
│      │                                                              │
│      │ 3. GET /api/products                                      │
│      ├────────────────────────────────────────────────────────►   │
│      │◄────────────────────────────────────────────────────────┤  │
│      │ 4. 200 OK                                                 │
│      │    Access-Control-Allow-Origin: http://localhost:3000   │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Phiên bản & Tài liệu

API Versioning

Cài đặt

dotnet add package Microsoft.AspNetCore.Mvc.Versioning

Cấu hình

// Program.cs
builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
    options.ApiVersionReader = ApiVersionReader.Combine(
        new UrlSegmentApiVersionReader(),
        new HeaderApiVersionReader("X-Api-Version"),
        new QueryStringApiVersionReader("v"));
});

Versioning Strategies

1. URL Path (phổ biến nhất)

[ApiVersion("1.0")]
[ApiVersion("2.0")]
[Route("api/v{v:apiVersion}/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet]
    [MapToApiVersion("1.0")]
    public IActionResult GetV1() => Ok(new { version = "1.0" });
    
    [HttpGet]
    [MapToApiVersion("2.0")]
    public IActionResult GetV2() => Ok(new { version = "2.0", extra = "data" });
}

2. Query String

GET /api/products?api-version=1.0
GET /api/products?api-version=2.0

3. Header

GET /api/products
X-Api-Version: 1.0

Deprecation

[ApiVersion("1.0")]
[ApiVersion("2.0", Deprecated = true)] // Mark as deprecated
[Route("api/v{v:apiVersion}/[controller]")]
public class ProductsController : ControllerBase { }

Swagger/OpenAPI

Cài đặt

dotnet add package Swashbuckle.AspNetCore

Cấu hình

// Program.cs
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo
    {
        Title = "My API",
        Version = "v1",
        Description = "API Description",
        Contact = new OpenApiContact
        {
            Name = "Support",
            Email = "support@example.com"
        }
    });
    
    // Add JWT Authentication to Swagger
    c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Description = "JWT Authorization header using the Bearer scheme",
        Name = "Authorization",
        In = ParameterLocation.Header,
        Type = SecuritySchemeType.ApiKey,
        Scheme = "Bearer"
    });
    
    c.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type = ReferenceType.SecurityScheme,
                    Id = "Bearer"
                }
            },
            Array.Empty<string>()
        }
    });
});

var app = builder.Build();

app.UseSwagger();
app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API v1");
    c.RoutePrefix = "swagger"; // Access at /swagger
});

XML Documentation

// Program.cs
builder.Services.AddSwaggerGen(c =>
{
    var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
    c.IncludeXmlComments(xmlPath);
});

/// <summary>
/// Get all products
/// </summary>
/// <param name="category">Filter by category</param>
/// <returns>List of products</returns>
[HttpGet]
[ProducesResponseType(typeof(List<Product>), 200)]
[ProducesResponseType(400)]
public IActionResult GetProducts([FromQuery] string category) { }

Swagger Response Attributes

[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult Create([FromBody] Product product)
{
    // ...
}

Best Practices cho API Versioning

1. Chọn chiến lược version phù hợp

StrategyProsCons
URL PathRõ ràng, dễ debugPhải update client URLs
Query StringKhông thay đổi URLÍt visible
HeaderLinh hoạtKhó debug

2. Support backwards compatibility

// ✅ Tốt - Add new fields, không break old
public class ProductResponse
{
    public int Id { get; set; }
    public string Name { get; set; }
    // New in v2
    public string Description { get; set; } 
}

// ❌ Tránh - Breaking changes
// - Remove fields
// - Change field types
// - Change validation rules

3. Document changes

# OpenAPI spec
components:
  schemas:
    ProductV1:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
    ProductV2:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        description:  # New field
          type: string

4. Deprecation policy

  • Thông báo deprecation trước khi remove
  • Sử dụng HTTP headers để warning
  • Cung cấp migration guide

4. Truy cập Dữ liệu với EF Core

Giới thiệu

Entity Framework Core là ORM (Object-Relational Mapper) của Microsoft, cho phép làm việc với database bằng cách sử dụng đối tượng C# thay vì SQL queries trực tiếp.

Nội dung chính

Cơ bản & Thiết kế

Tối ưu hiệu suất

Giao dịch & Đồng thời

EF Core - Cơ bản & Thiết kế

Code First vs Database First

Code First

Tạo database từ C# classes:

// Models
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int CategoryId { get; set; }
    public Category Category { get; set; }
}

public class Category
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<Product> Products { get; set; }
}

// DbContext
public class AppDbContext : DbContext
{
    public DbSet<Product> Products { get; set; }
    public DbSet<Category> Categories { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlServer("ConnectionString");
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Product>(entity =>
        {
            entity.HasKey(p => p.Id);
            entity.Property(p => p.Name).IsRequired().HasMaxLength(100);
            entity.Property(p => p.Price).HasColumnType("decimal(18,2)");
        });
    }
}

Database First

Reverse engineer từ existing database:

# Scaffold từ database
dotnet ef dbcontext scaffold "ConnectionString" Microsoft.EntityFrameworkCore.SqlServer

# Với options
dotnet ef dbcontext scaffold "ConnectionString" Microsoft.EntityFrameworkCore.SqlServer `
    --table Products,Categories `
    --context AppDbContext `
    --output-dir Models

Migrations

Cài đặt

dotnet tool install --global dotnet-ef
dotnet add package Microsoft.EntityFrameworkCore.Design

Commands

# Tạo migration
dotnet ef migrations add InitialCreate

# Apply migrations
dotnet ef database update

# Update với migration cụ thể
dotnet ef database update 20240101000000_InitialCreate

# Rollback
dotnet ef database update PreviousMigrationName

# Remove last migration
dotnet ef migrations remove

# List migrations
dotnet ef migrations list

# Generate SQL script
dotnet ef migrations script

Migration Structure

public partial class InitialCreate : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateTable(
            name: "Categories",
            columns: table => new
            {
                Id = table.Column<int>(type: "int", nullable: false)
                    .Annotation("SqlServer:Identity", "1, 1"),
                Name = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_Categories", x => x.Id);
            });
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropTable(name: "Categories");
    }
}

DbContext và Change Tracker

Change Tracker

EF Core theo dõi các thay đổi trên entities:

var context = new AppDbContext();

// Read
var product = context.Products.First(); // Tracked

// Update
product.Price = 99.99m;
// EF tự động mark là Modified

// Delete
context.Products.Remove(product);
// EF mark là Deleted

// SaveChanges - Tạo SQL UPDATE/DELETE
context.SaveChanges();

Entity States

StateDescription
DetachedKhông được track
AddedMới, chưa có trong database
UnchangedKhông thay đổi
ModifiedĐã thay đổi
DeletedĐánh dấu xóa

Tracking Behavior

// Tracked (default)
var product = context.Products.First();

// No tracking - tốt cho read-only
var product = context.Products.AsNoTracking().First();

// Chỉ định rõ ràng tracking
var product = context.Products.AsTracking().First();

// Kiểm tra state
var entry = context.Entry(product);
Console.WriteLine(entry.State); // Modified

DbContext Lifetime

// ✅ Tốt - Scoped cho mỗi request
builder.Services.AddScoped<AppDbContext>();

// ❌ Tránh - Singleton
builder.Services.AddSingleton<AppDbContext>(); // Bad practice!

Relationships Configuration

One-to-Many

public class Author
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<Book> Books { get; set; }
}

public class Book
{
    public int Id { get; set; }
    public string Title { get; set; }
    public int AuthorId { get; set; }
    public Author Author { get; set; }
}

// Configuration
modelBuilder.Entity<Book>()
    .HasOne(b => b.Author)
    .WithMany(a => a.Books)
    .HasForeignKey(b => b.AuthorId);

One-to-One

public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    public StudentProfile Profile { get; set; }
}

public class StudentProfile
{
    public int Id { get; set; }
    public string Bio { get; set; }
    public int StudentId { get; set; }
    public Student Student { get; set; }
}

// Configuration
modelBuilder.Entity<Student>()
    .HasOne(s => s.Profile)
    .WithOne(p => p.Student)
    .HasForeignKey<StudentProfile>(p => p.StudentId);

Many-to-Many

public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<StudentCourse> StudentCourses { get; set; }
}

public class Course
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<StudentCourse> StudentCourses { get; set; }
}

// Junction table
public class StudentCourse
{
    public int StudentId { get; set; }
    public Student Student { get; set; }
    public int CourseId { get; set; }
    public Course Course { get; set; }
}

// Cấu hình composite key
modelBuilder.Entity<StudentCourse>()
    .HasKey(sc => new { sc.StudentId, sc.CourseId });

EF Core - Tối ưu Hiệu suất

N+1 Query Problem

Vấn đề

N+1 xảy ra khi bạn load một list objects, sau đó access navigation property của từng object (lazy loading):

// ❌ BAD - N+1 Query
var products = context.Products.ToList();  // 1 query

foreach (var product in products)
{
    Console.WriteLine(product.Category.Name); // N queries!
}

// Generated SQL:
// SELECT * FROM Products        -- 1 query
// SELECT * FROM Categories WHERE Id = 1  -- N queries!

Giải pháp

1. Eager Loading với Include

// ✅ Sử dụng Include
var products = context.Products
    .Include(p => p.Category)
    .ToList();

// Generated SQL:
// SELECT p.*, c.* FROM Products p
// LEFT JOIN Categories c ON p.CategoryId = c.Id

2. ThenInclude cho nested relationships

var orders = context.Orders
    .Include(o => o.Customer)
        .ThenInclude(c => c.Address)
    .Include(o => o.OrderItems)
        .ThenInclude(oi => oi.Product)
            .ThenInclude(p => p.Category)
    .ToList();

3. Explicit Loading

var product = context.Products.First();

// Load Category explicitly
await context.Entry(product)
    .Reference(p => p.Category)
    .LoadAsync();

// Load Collection explicitly  
await context.Entry(product)
    .Collection(p => p.Reviews)
    .LoadAsync();

4. Projections (Select)

// ✅ Tốt - Chỉ lấy những gì cần
var productDtos = context.Products
    .Select(p => new ProductDto
    {
        Id = p.Id,
        Name = p.Name,
        CategoryName = p.Category.Name
    })
    .ToList();

// Generated SQL: Chỉ select Id, Name, CategoryName

AsNoTracking

Khi nào sử dụng

// ✅ Sử dụng AsNoTracking() cho read-only queries
var products = context.Products
    .AsNoTracking()
    .Where(p => p.Price > 100)
    .ToList();

// ✅ Hoặc với AsNoTrackingWithIdentityResolution
var products = context.Products
    .AsNoTrackingWithIdentityResolution()
    .Include(p => p.Category)
    .ToList();

So sánh

MethodTrackingIdentity ResolutionPerformance
AsNoTracking()❌ No❌ NoFastest
AsTracking()✅ Yes✅ YesDefault
AsNoTrackingWithIdentityResolution()❌ No✅ YesFast

Lưu ý

// ❌ AsNoTracking - Không thể save changes
var product = context.Products.AsNoTracking().First();
product.Name = "New Name";
context.SaveChanges(); // Không có gì thay đổi!

// ✅ AsTracking - Có thể save changes  
var product = context.Products.First();
product.Name = "New Name";
context.SaveChanges(); // Update thành công

Compiled Queries

Cache query plan

// Đăng ký compiled query
private static readonly Func<AppDbContext, IQueryable<Product>> 
    GetAllProducts = EF.CompileQuery((AppDbContext ctx) => 
        ctx.Products.Include(p => p.Category));

// Sử dụng
using (var context = new AppDbContext())
{
    var products = GetAllProducts(context).ToList();
}

Pagination

Skip/Take

var page = 1;
var pageSize = 10;

var products = context.Products
    .OrderBy(p => p.Name)
    .Skip((page - 1) * pageSize)
    .Take(pageSize)
    .ToList();

Batch Size

// Cấu hình batch size
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
    options.UseSqlServer(
        "ConnectionString",
        options => options.MaxBatchSize(100));
}

Split Queries

Giải quyết vấn đề cartesian product

// ✅ Sử dụng SplitQuery khi có nhiều collections
var orders = context.Orders
    .Include(o => o.OrderItems)
    .AsSplitQuery()
    .ToList();

// Generated SQL: 2 queries thay vì 1 với cartesian product

Raw SQL Queries

FormattedRawSql

// Raw SQL với parameters
var products = context.Products
    .FromSqlRaw("SELECT * FROM Products WHERE Price > {0}", minPrice)
    .ToList();

// Stored Procedure
var products = context.Products
    .FromSqlRaw("EXEC GetProducts @CategoryId = {0}", categoryId)
    .ToList();

Bulk Operations

Install package

dotnet add package EFCore.BulkExtensions

Bulk Insert

var entities = Enumerable.Range(0, 1000)
    .Select(i => new Product { Name = $"Product {i}", Price = i })
    .ToList();

context.BulkInsert(entities);

Bulk Update

context.BulkUpdate(entities);

Bulk Delete

context.BulkDelete(entitiesToDelete);

EF Core - Giao dịch & Đồng thời

Transactions

Basic Transaction

using var transaction = await context.Database.BeginTransactionAsync();

try
{
    var order = new Order { CustomerId = 1 };
    context.Orders.Add(order);
    await context.SaveChangesAsync();
    
    var orderItem = new OrderItem { OrderId = order.Id, ProductId = 1 };
    context.OrderItems.Add(orderItem);
    await context.SaveChangesAsync();
    
    await transaction.CommitAsync();
}
catch (Exception ex)
{
    await transaction.RollbackAsync();
    throw;
}

Transaction với Isolation Level

var transaction = await context.Database.BeginTransactionAsync(
    System.Data.IsolationLevel.Serializable);

try
{
    // Operations với Serializable isolation
    await transaction.CommitAsync();
}
finally
{
    await transaction.DisposeAsync();
}

Transaction với Database Transaction

await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();

await using var transaction = await connection.BeginTransactionAsync();
try
{
    var command = connection.CreateCommand();
    command.Transaction = transaction;
    command.CommandText = "INSERT INTO Products VALUES (@name, @price)";
    
    var param1 = command.CreateParameter();
    param1.ParameterName = "@name";
    param1.Value = "Product";
    
    var param2 = command.CreateParameter();
    param2.ParameterName = "@price";
    param2.Value = 9.99m;
    
    command.Parameters.Add(param1);
    command.Parameters.Add(param2);
    
    await command.ExecuteNonQueryAsync();
    
    await transaction.CommitAsync();
}
catch
{
    await transaction.RollbackAsync();
    throw;
}

Concurrency (Xung đột dữ liệu)

Vấn đề Concurrency

User A reads product (Version = 1)
User B reads product (Version = 1)
User A updates price to 50, saves (Version = 1 → 2)
User B updates price to 60, saves (Version = 1 → ?)
                              ↓
                    CONFLICT - User B should fail!

Giải pháp: RowVersion/Timestamp

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    
    [Timestamp]
    public byte[] RowVersion { get; set; }
}

Cấu hình Fluent API

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Product>()
        .Property(p => p.RowVersion)
        .IsRowVersion(); // SQL Server会自动使用 rowversion
    
    // Hoặc cho các database khác
    modelBuilder.Entity<Product>()
        .Property(p => p.RowVersion)
        .IsConcurrencyToken();
}

Xử lý DbUpdateConcurrencyException

public async Task<bool> UpdateProduct(Product product)
{
    try
    {
        context.Products.Update(product);
        await context.SaveChangesAsync();
        return true;
    }
    catch (DbUpdateConcurrencyException ex)
    {
        // Get entry để xử lý
        var entry = ex.Entries.Single();
        
        // Lấy database values
        var databaseValues = await entry.GetDatabaseValuesAsync();
        
        // Option 1: Reload từ database
        await entry.ReloadAsync();
        
        // Option 2: Merge với client values
        var clientValues = entry.CurrentValues;
        var databaseValues = await entry.GetDatabaseValuesAsync();
        
        // Log hoặc thông báo cho user
        Console.WriteLine("Concurrency conflict detected!");
        
        return false;
    }
}

Retry on Concurrency

public async Task<bool> UpdateProductWithRetry(Product product)
{
    var retryCount = 0;
    const int maxRetries = 3;
    
    while (retryCount < maxRetries)
    {
        try
        {
            context.Products.Update(product);
            await context.SaveChangesAsync();
            return true;
        }
        catch (DbUpdateConcurrencyException)
        {
            retryCount++;
            if (retryCount >= maxRetries) throw;
            
            // Reload và retry
            var entry = context.Entry(product);
            await entry.ReloadAsync();
        }
    }
    
    return false;
}

Client-Side Concurrency Token

Custom concurrency token

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    
    // Sử dụng property khác làm concurrency token
    public DateTime UpdatedAt { get; set; }
}

// Cấu hình
modelBuilder.Entity<Product>()
    .Property(p => p.UpdatedAt)
    .IsConcurrencyToken();

Optimistic vs Pessimistic Concurrency

Optimistic (EF Core default)

  • Không lock records
  • Kiểm tra version khi save
  • Nếu conflict → exception
  • Phù hợp cho low-contention scenarios

Pessimistic

  • Lock records trước khi update
  • Sử dụng explicit transactions
  • Phù hợp cho high-contention scenarios
// Pessimistic lock example
await using var transaction = await context.Database.BeginTransactionAsync();

var product = await context.Products
    .FromSqlRaw("SELECT * FROM Products WITH (UPDLOCK) WHERE Id = {0}", id)
    .FirstAsync();

// Update product
product.Price = newPrice;
await context.SaveChangesAsync();

await transaction.CommitAsync();

5. Kiến trúc Phần mềm

Giới thiệu

Phần này trình bày các nguyên tắc, pattern, và kiến trúc phần mềm phổ biến trong phát triển ứng dụng .NET. Từ các nguyên tắc thiết kế cơ bản đến các kiến trúc hệ thống phức tạp, phần này cung cấp kiến thức toàn diện để xây dựng ứng dụng bền vững, dễ bảo trì và mở rộng.

Cấu trúc nội dung

1. Nguyên tắc Thiết kế

Các nguyên tắc cơ bản hướng dẫn việc thiết kế phần mềm chất lượng cao:

  • SOLID Principles - 5 nguyên tắc thiết kế hướng đối tượng cơ bản
  • DRY, KISS, YAGNI - Nguyên tắc đơn giản hóa và tránh trùng lặp
  • Separation of Concerns - Phân tách mối quan tâm trong thiết kế

2. Design Patterns

Các mẫu thiết kế đã được chứng minh giải quyết các vấn đề phổ biến:

  • Creational Patterns - Factory, Singleton, Builder, Prototype
  • Structural Patterns - Adapter, Decorator, Facade, Proxy
  • Behavioral Patterns - Strategy, Observer, Command, Mediator

3. Kiến trúc Ứng dụng

Các kiến trúc tổ chức codebase và phân tách responsibility:

  • Clean Architecture - Phân tách layer với Domain ở trung tâm
  • Domain-Driven Design (DDD) - Thiết kế tập trung vào nghiệp vụ
  • CQRS - Tách biệt Command và Query responsibility
  • Event-Driven Architecture - Kiến trúc dựa trên sự kiện
  • Hexagonal Architecture - Kiến trúc cổng và adapter
  • Onion Architecture - Kiến trúc vòng tròn đồng tâm

4. Phương pháp Phát triển

Các phương pháp luận trong phát triển phần mềm:

  • Test-Driven Development (TDD) - Phát triển dựa trên kiểm thử
  • Behavior-Driven Development (BDD) - Phát triển dựa trên hành vi

5. Kiến trúc Hệ thống

Các kiến trúc ở cấp độ hệ thống và phân tán:

  • Microservices - Kiến trúc dịch vụ nhỏ độc lập
  • Monolithic Architecture - Kiến trúc nguyên khối truyền thống
  • Serverless Architecture - Kiến trúc không máy chủ
  • Event Sourcing - Lưu trữ trạng thái dưới dạng chuỗi sự kiện

Mục tiêu học tập

Sau khi hoàn thành phần này, bạn sẽ có khả năng:

  1. Hiểu và áp dụng các nguyên tắc SOLID trong thiết kế class
  2. Lựa chọn và triển khai design patterns phù hợp
  3. Thiết kế kiến trúc ứng dụng theo Clean Architecture, DDD, CQRS
  4. Áp dụng TDD trong phát triển phần mềm
  5. Lựa chọn kiến trúc hệ thống phù hợp với yêu cầu dự án
  6. Thiết kế hệ thống phân tán với microservices và message queue

Nguyên tắc Thiết kế

Overview

Các nguyên tắc thiết kế (design principles) là các guidelines giúp tạo ra code clean, maintainable, và scalable. Chúng là nền tảng cho việc xây dựng phần mềm chất lượng cao.

SOLID Principles

1. Single Responsibility Principle (SRP)

Một class chỉ nên có một lý do để thay đổi.

// Bad - Multiple responsibilities
public class User
{
    public void Save() { /* Save to database */ }
    public void Validate() { /* Validation logic */ }
    public void SendEmail() { /* Email sending */ }
    public void GenerateReport() { /* Report generation */ }
}

// Good - Single responsibility
public class User { } // Just data

public class UserRepository
{
    public void Save(User user) { /* Database */ }
}

public class UserValidator
{
    public bool Validate(User user) { /* Validation */ }
}

public class EmailService
{
    public void SendEmail(User user) { /* Email */ }
}

2. Open/Closed Principle (OCP)

Entities nên open cho extension nhưng closed cho modification.

// Bad - Need to modify to add new behavior
public class OrderCalculator
{
    public decimal Calculate(Order order)
    {
        if (order.Type == OrderType.Standard)
            return order.Amount * 1.0m;
        else if (order.Type == OrderType.Premium)
            return order.Amount * 0.9m;
        // Must add new condition for new types
    }
}

// Good - Extend without modification
public interface IDiscountStrategy
{
    decimal Calculate(Order order);
}

public class StandardDiscount : IDiscountStrategy
{
    public decimal Calculate(Order order) => order.Amount * 1.0m;
}

public class PremiumDiscount : IDiscountStrategy
{
    public decimal Calculate(Order order) => order.Amount * 0.9m;
}

public class OrderCalculator
{
    private readonly IDiscountStrategy _discountStrategy;
    
    public OrderCalculator(IDiscountStrategy discountStrategy)
    {
        _discountStrategy = discountStrategy;
    }
    
    public decimal Calculate(Order order) 
        => _discountStrategy.Calculate(order);
}

3. Liskov Substitution Principle (LSP)

Objects của superclass nên có thể thay thế bằng objects của subclass mà không làm break application.

// Bad - Violates LSP
public class Rectangle
{
    public virtual int Width { get; set; }
    public virtual int Height { get; set; }
    public int Area => Width * Height;
}

public class Square : Rectangle
{
    public override int Width 
    {
        set { base.Width = value; base.Height = value; }
    }
    public override int Height
    {
        set { base.Width = value; base.Height = value; }
    }
}

// This will break!
void CalculateArea(Rectangle rect)
{
    rect.Width = 5;
    rect.Height = 4;
    Console.WriteLine(rect.Area); // Expects 20, but gets 16 for Square!
}

// Good - Proper inheritance
public interface IShape
{
    int Area { get; }
}

public class Rectangle : IShape
{
    public int Width { get; set; }
    public int Height { get; set; }
    public int Area => Width * Height;
}

public class Circle : IShape
{
    public int Radius { get; set; }
    public int Area => (int)(Math.PI * Radius * Radius);
}

4. Interface Segregation Principle (ISP)

Nên prefer many specific interfaces hơn là một interface general.

// Bad - Fat interface
public interface IWorker
{
    void Work();
    void Eat();
    void Sleep();
}

public class Robot : IWorker
{
    public void Work() { }
    public void Eat() { throw new NotImplementedException(); } // Doesn't eat
    public void Sleep() { throw new NotImplementedException(); } // Doesn't sleep
}

// Good - Segregated interfaces
public interface IWorkable
{
    void Work();
}

public interface IEatable
{
    void Eat();
}

public interface ISleepable
{
    void Sleep();
}

public class Human : IWorkable, IEatable, ISleepable
{
    public void Work() { }
    public void Eat() { }
    public void Sleep() { }
}

public class Robot : IWorkable
{
    public void Work() { }
}

5. Dependency Inversion Principle (DIP)

Nên depend on abstractions, không phải concretions.

// Bad - Depend on concrete class
public class OrderService
{
    private readonly SqlOrderRepository _repository; // Hard dependency
    
    public OrderService()
    {
        _repository = new SqlOrderRepository();
    }
}

// Good - Depend on abstraction
public interface IOrderRepository
{
    Task<Order> GetByIdAsync(Guid id);
}

public class OrderService
{
    private readonly IOrderRepository _repository; // Abstraction
    
    public OrderService(IOrderRepository repository)
    {
        _repository = repository;
    }
}

// Can inject any implementation
public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IOrderRepository, SqlOrderRepository>();
    // Or switch to MongoDB easily
    // services.AddScoped<IOrderRepository, MongoOrderRepository>();
}

Other Important Principles

DRY - Don’t Repeat Yourself

// Don't repeat logic
public string FormatEmail(string name, string orderId)
{
    return $"Dear {name}, your order {orderId} has been placed.";
}

public string FormatEmail(string name, string orderId, string status)
{
    return $"Dear {name}, your order {orderId} is now {status}.";
}

// Extract to shared method
public static class EmailFormatter
{
    public static string Format(string name, string orderId, string status = null)
    {
        return status == null 
            ? $"Dear {name}, your order {orderId} has been placed."
            : $"Dear {name}, your order {orderId} is now {status}.";
    }
}

KISS - Keep It Simple, Stupid

// Simple, readable code over clever code
// Bad
var result = items?.Where(x => x > 0)?.Sum() ?? 0;

// Good - clear intent
if (items == null || items.Count == 0)
    return 0;
    
return items.Where(x => x > 0).Sum();

YAGNI - You Aren’t Gonna Need It

// Don't add functionality that isn't needed yet
// Bad
public interface IUserRepository
{
    User GetById(Guid id);
    User GetByEmail(string email);  // Not used!
    User GetByPhone(string phone);  // Not used!
    List<User> Search(string query); // Not used!
}

// Good - Add as needed
public interface IUserRepository
{
    User GetById(Guid id);
    // Add more methods when actually needed
}

Law of Demeter

Chỉ nói chuyện với “friends”, không phải “friends of friends”.

// Bad - Violates Law of Demeter
var address = customer.GetAddress().GetCity().GetName();

// Good - Ask for what you need
var cityName = customer.GetCityName();

Summary

PrincipleDescription
SRPMột class, một trách nhiệm
OCPOpen for extension, closed for modification
LSPThay thế được subclass
ISPNhiều interface nhỏ, không interface lớn
DIPDepend on abstractions

References

SOLID Principles

Giới thiệu

SOLID là tập hợp 5 nguyên tắc thiết kế hướng đối tượng giúp tạo ra code dễ bảo trì, mở rộng và tái sử dụng. Các nguyên tắc này được đặt ra bởi Robert C. Martin (Uncle Bob) và đã trở thành tiêu chuẩn trong ngành công nghiệp phần mềm.

1. Single Responsibility Principle (SRP)

Định nghĩa

“Một class chỉ nên có một lý do để thay đổi” - mỗi class chỉ nên đảm nhận một responsibility duy nhất.

Tại sao cần SRP?

  • Giảm sự phụ thuộc giữa các phần của hệ thống
  • Dễ dàng test và debug
  • Giảm thiểu tác động khi thay đổi requirement
  • Code dễ đọc và bảo trì hơn

Ví dụ vi phạm SRP

// ❌ VI PHẠM - Class có nhiều responsibilities
public class OrderProcessor
{
    public void ProcessOrder(Order order)
    {
        // 1. Validate order
        if (!ValidateOrder(order))
            throw new InvalidOrderException();
        
        // 2. Save to database
        SaveToDatabase(order);
        
        // 3. Send confirmation email
        SendEmail(order);
        
        // 4. Generate invoice
        GenerateInvoice(order);
        
        // 5. Update inventory
        UpdateInventory(order);
    }
    
    private bool ValidateOrder(Order order) { /* validation logic */ }
    private void SaveToDatabase(Order order) { /* database logic */ }
    private void SendEmail(Order order) { /* email logic */ }
    private void GenerateInvoice(Order order) { /* invoice logic */ }
    private void UpdateInventory(Order order) { /* inventory logic */ }
}

Ví dụ tuân thủ SRP

// ✅ TUÂN THỦ - Tách thành các classes chuyên biệt
public class OrderValidator
{
    public bool Validate(Order order) { /* validation logic */ }
}

public class OrderRepository
{
    public void Save(Order order) { /* database logic */ }
}

public class EmailService
{
    public void SendOrderConfirmation(Order order) { /* email logic */ }
}

public class InvoiceService
{
    public void Generate(Order order) { /* invoice logic */ }
}

public class InventoryService
{
    public void Update(Order order) { /* inventory logic */ }
}

// Class chính chỉ điều phối
public class OrderProcessor
{
    private readonly OrderValidator _validator;
    private readonly OrderRepository _repository;
    private readonly EmailService _emailService;
    private readonly InvoiceService _invoiceService;
    private readonly InventoryService _inventoryService;
    
    public OrderProcessor(
        OrderValidator validator,
        OrderRepository repository,
        EmailService emailService,
        InvoiceService invoiceService,
        InventoryService inventoryService)
    {
        _validator = validator;
        _repository = repository;
        _emailService = emailService;
        _invoiceService = invoiceService;
        _inventoryService = inventoryService;
    }
    
    public void ProcessOrder(Order order)
    {
        if (!_validator.Validate(order))
            throw new InvalidOrderException();
        
        _repository.Save(order);
        _emailService.SendOrderConfirmation(order);
        _invoiceService.Generate(order);
        _inventoryService.Update(order);
    }
}

Thực hành với ASP.NET Core

// ✅ SRP trong Controller
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    private readonly IOrderService _orderService;
    
    public OrdersController(IOrderService orderService)
    {
        _orderService = orderService;
    }
    
    [HttpPost]
    public async Task<IActionResult> Create([FromBody] CreateOrderDto dto)
    {
        // Controller chỉ xử lý HTTP concerns
        var result = await _orderService.CreateOrderAsync(dto);
        return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
    }
}

// Service xử lý business logic
public class OrderService : IOrderService
{
    private readonly IOrderValidator _validator;
    private readonly IOrderRepository _repository;
    private readonly IInventoryService _inventoryService;
    
    public async Task<OrderResult> CreateOrderAsync(CreateOrderDto dto)
    {
        // Business logic tập trung ở đây
        var order = Order.Create(dto);
        
        if (!await _validator.ValidateAsync(order))
            throw new ValidationException("Invalid order");
        
        await _repository.AddAsync(order);
        await _inventoryService.ReserveItemsAsync(order.Items);
        
        return OrderResult.FromOrder(order);
    }
}

2. Open/Closed Principle (OCP)

Định nghĩa

“Các entities (class, module, function) nên mở cho việc mở rộng nhưng đóng cho việc sửa đổi.”

Tại sao cần OCP?

  • Giảm rủi ro khi thêm tính năng mới
  • Dễ dàng mở rộng mà không ảnh hưởng đến code hiện có
  • Tuân thủ OCP thường dẫn đến thiết kế tốt hơn

Ví dụ vi phạm OCP

// ❌ VI PHẠM - Phải sửa code khi thêm payment method mới
public class PaymentProcessor
{
    public void ProcessPayment(Payment payment)
    {
        switch (payment.Type)
        {
            case PaymentType.CreditCard:
                ProcessCreditCard(payment);
                break;
            case PaymentType.PayPal:
                ProcessPayPal(payment);
                break;
            case PaymentType.BankTransfer:
                ProcessBankTransfer(payment);
                break;
            // Thêm method mới => phải sửa switch statement!
            default:
                throw new ArgumentException("Unsupported payment type");
        }
    }
    
    private void ProcessCreditCard(Payment payment) { /* logic */ }
    private void ProcessPayPal(Payment payment) { /* logic */ }
    private void ProcessBankTransfer(Payment payment) { /* logic */ }
}

Ví dụ tuân thủ OCP

// ✅ TUÂN THỦ - Sử dụng abstraction và polymorphism
public interface IPaymentMethod
{
    Task<PaymentResult> ProcessAsync(decimal amount);
}

public class CreditCardPayment : IPaymentMethod
{
    public async Task<PaymentResult> ProcessAsync(decimal amount)
    {
        // Credit card processing logic
        return new PaymentResult { Success = true };
    }
}

public class PayPalPayment : IPaymentMethod
{
    public async Task<PaymentResult> ProcessAsync(decimal amount)
    {
        // PayPal processing logic
        return new PaymentResult { Success = true };
    }
}

public class BankTransferPayment : IPaymentMethod
{
    public async Task<PaymentResult> ProcessAsync(decimal amount)
    {
        // Bank transfer logic
        return new PaymentResult { Success = true };
    }
}

// Thêm payment method mới không cần sửa PaymentProcessor
public class CryptoPayment : IPaymentMethod
{
    public async Task<PaymentResult> ProcessAsync(decimal amount)
    {
        // Cryptocurrency processing logic
        return new PaymentResult { Success = true };
    }
}

public class PaymentProcessor
{
    private readonly Dictionary<PaymentType, IPaymentMethod> _paymentMethods;
    
    public PaymentProcessor()
    {
        _paymentMethods = new Dictionary<PaymentType, IPaymentMethod>
        {
            { PaymentType.CreditCard, new CreditCardPayment() },
            { PaymentType.PayPal, new PayPalPayment() },
            { PaymentType.BankTransfer, new BankTransferPayment() },
            { PaymentType.Crypto, new CryptoPayment() } // Thêm mới dễ dàng
        };
    }
    
    public async Task<PaymentResult> ProcessPayment(Payment payment)
    {
        if (_paymentMethods.TryGetValue(payment.Type, out var paymentMethod))
        {
            return await paymentMethod.ProcessAsync(payment.Amount);
        }
        
        throw new ArgumentException("Unsupported payment type");
    }
}

Strategy Pattern với OCP

// Sử dụng Strategy Pattern để tuân thủ OCP
public interface IDiscountStrategy
{
    decimal CalculateDiscount(decimal originalPrice);
}

public class NoDiscountStrategy : IDiscountStrategy
{
    public decimal CalculateDiscount(decimal originalPrice) => originalPrice;
}

public class PercentageDiscountStrategy : IDiscountStrategy
{
    private readonly decimal _percentage;
    
    public PercentageDiscountStrategy(decimal percentage)
    {
        _percentage = percentage;
    }
    
    public decimal CalculateDiscount(decimal originalPrice)
        => originalPrice * (1 - _percentage / 100);
}

public class FixedAmountDiscountStrategy : IDiscountStrategy
{
    private readonly decimal _amount;
    
    public FixedAmountDiscountStrategy(decimal amount)
    {
        _amount = amount;
    }
    
    public decimal CalculateDiscount(decimal originalPrice)
        => Math.Max(0, originalPrice - _amount);
}

public class PriceCalculator
{
    private readonly IDiscountStrategy _discountStrategy;
    
    public PriceCalculator(IDiscountStrategy discountStrategy)
    {
        _discountStrategy = discountStrategy;
    }
    
    public decimal CalculateFinalPrice(decimal originalPrice)
    {
        return _discountStrategy.CalculateDiscount(originalPrice);
    }
}

// Sử dụng
var calculator = new PriceCalculator(new PercentageDiscountStrategy(20));
var finalPrice = calculator.CalculateFinalPrice(100); // 80

3. Liskov Substitution Principle (LSP)

Định nghĩa

“Các objects của subclass phải có thể thay thế objects của parent class mà không làm thay đổi tính đúng đắn của chương trình.”

Tại sao cần LSP?

  • Đảm bảo tính đúng đắn của inheritance hierarchy
  • Ngăn ngừa lỗi runtime khi thay thế objects
  • Giúp thiết kế interface hợp lý hơn

Ví dụ vi phạm LSP (Hình học cổ điển)

public class Rectangle
{
    public virtual int Width { get; set; }
    public virtual int Height { get; set; }
    
    public int Area => Width * Height;
}

public class Square : Rectangle
{
    private int _side;
    
    public override int Width
    {
        get => _side;
        set { _side = value; Height = value; }
    }
    
    public override int Height
    {
        get => _side;
        set { _side = value; Width = value; }
    }
}

// ❌ VI PHẠM LSP - Square không thể thay thế Rectangle
void TestRectangleArea(Rectangle rect)
{
    rect.Width = 5;
    rect.Height = 4;
    
    // Với Rectangle: 5 * 4 = 20
    // Với Square: 4 * 4 = 16 (sau khi set Height = 4, Width cũng thành 4)
    Console.WriteLine($"Area: {rect.Area}");
}

var rectangle = new Rectangle();
TestRectangleArea(rectangle); // Area: 20 ✅

var square = new Square();
TestRectangleArea(square); // Area: 16 ❌ (không đúng kỳ vọng)

Giải pháp cho LSP

// ✅ TUÂN THỦ - Sử dụng interface hoặc abstract class chung
public interface IShape
{
    int Area { get; }
}

public class Rectangle : IShape
{
    public int Width { get; set; }
    public int Height { get; set; }
    
    public int Area => Width * Height;
}

public class Square : IShape
{
    public int Side { get; set; }
    
    public int Area => Side * Side;
}

// Hoặc sử dụng composition thay vì inheritance
public abstract class Shape
{
    public abstract int Area { get; }
}

public class Rectangle : Shape
{
    public int Width { get; }
    public int Height { get; }
    
    public Rectangle(int width, int height)
    {
        Width = width;
        Height = height;
    }
    
    public override int Area => Width * Height;
}

public class Square : Shape
{
    public int Side { get; }
    
    public Square(int side)
    {
        Side = side;
    }
    
    public override int Area => Side * Side;
}

Ví dụ thực tế với Repository Pattern

// ❌ VI PHẠM - Repository vi phạm LSP
public interface IRepository<T>
{
    T GetById(int id);
    void Save(T entity);
    void Delete(int id);
}

public class UserRepository : IRepository<User>
{
    public User GetById(int id) { /* logic */ }
    public void Save(User user) { /* logic */ }
    public void Delete(int id) { /* logic */ }
}

public class ReadOnlyUserRepository : IRepository<User>
{
    public User GetById(int id) { /* logic */ }
    
    public void Save(User user)
    {
        throw new NotSupportedException("Read-only repository");
    }
    
    public void Delete(int id)
    {
        throw new NotSupportedException("Read-only repository");
    }
}

// ✅ TUÂN THỦ - Tách interface
public interface IReadRepository<T>
{
    T GetById(int id);
    IEnumerable<T> GetAll();
}

public interface IWriteRepository<T>
{
    void Save(T entity);
    void Delete(int id);
}

public interface IRepository<T> : IReadRepository<T>, IWriteRepository<T>
{
}

public class UserRepository : IRepository<User>
{
    // Implement cả read và write
}

public class ReadOnlyUserRepository : IReadRepository<User>
{
    // Chỉ implement read operations
    public User GetById(int id) { /* logic */ }
    public IEnumerable<User> GetAll() { /* logic */ }
}

4. Interface Segregation Principle (ISP)

Định nghĩa

“Client không nên bị buộc phải phụ thuộc vào các interface mà họ không sử dụng.”

Tại sao cần ISP?

  • Giảm sự phụ thuộc không cần thiết
  • Tránh “fat interfaces” với nhiều methods không liên quan
  • Dễ dàng test và mock

Ví dụ vi phạm ISP

// ❌ VI PHẠM - Fat interface
public interface IWorker
{
    void Work();
    void Eat();
    void Sleep();
    void Code();
    void Test();
    void Deploy();
}

public class HumanWorker : IWorker
{
    public void Work() { Console.WriteLine("Working..."); }
    public void Eat() { Console.WriteLine("Eating..."); }
    public void Sleep() { Console.WriteLine("Sleeping..."); }
    public void Code() { Console.WriteLine("Coding..."); }
    public void Test() { Console.WriteLine("Testing..."); }
    public void Deploy() { Console.WriteLine("Deploying..."); }
}

public class RobotWorker : IWorker
{
    public void Work() { Console.WriteLine("Working..."); }
    public void Eat() { /* Robot doesn't eat! */ }
    public void Sleep() { /* Robot doesn't sleep! */ }
    public void Code() { Console.WriteLine("Coding..."); }
    public void Test() { Console.WriteLine("Testing..."); }
    public void Deploy() { Console.WriteLine("Deploying..."); }
}

Ví dụ tuân thủ ISP

// ✅ TUÂN THỦ - Tách thành các interfaces nhỏ
public interface IWorkable
{
    void Work();
}

public interface IFeedable
{
    void Eat();
}

public interface ISleepable
{
    void Sleep();
}

public interface ICodeable
{
    void Code();
}

public interface ITestable
{
    void Test();
}

public interface IDeployable
{
    void Deploy();
}

// Human có thể làm mọi thứ
public class HumanWorker : IWorkable, IFeedable, ISleepable, ICodeable, ITestable, IDeployable
{
    public void Work() { Console.WriteLine("Working..."); }
    public void Eat() { Console.WriteLine("Eating..."); }
    public void Sleep() { Console.WriteLine("Sleeping..."); }
    public void Code() { Console.WriteLine("Coding..."); }
    public void Test() { Console.WriteLine("Testing..."); }
    public void Deploy() { Console.WriteLine("Deploying..."); }
}

// Robot chỉ làm việc liên quan đến coding
public class RobotWorker : IWorkable, ICodeable, ITestable, IDeployable
{
    public void Work() { Console.WriteLine("Working..."); }
    public void Code() { Console.WriteLine("Coding..."); }
    public void Test() { Console.WriteLine("Testing..."); }
    public void Deploy() { Console.WriteLine("Deploying..."); }
}

ISP trong ASP.NET Core

// ❌ VI PHẠM - Service interface quá lớn
public interface IOrderService
{
    Task<Order> CreateOrderAsync(CreateOrderDto dto);
    Task<Order> GetOrderAsync(int id);
    Task<IEnumerable<Order>> GetOrdersAsync();
    Task UpdateOrderAsync(int id, UpdateOrderDto dto);
    Task DeleteOrderAsync(int id);
    Task<Invoice> GenerateInvoiceAsync(int orderId);
    Task SendOrderConfirmationAsync(int orderId);
    Task CancelOrderAsync(int orderId);
    Task RefundOrderAsync(int orderId);
    Task<OrderStatistics> GetStatisticsAsync(DateTime from, DateTime to);
}

// ✅ TUÂN THỦ - Tách thành các interfaces chuyên biệt
public interface IOrderCommandService
{
    Task<Order> CreateOrderAsync(CreateOrderDto dto);
    Task UpdateOrderAsync(int id, UpdateOrderDto dto);
    Task DeleteOrderAsync(int id);
    Task CancelOrderAsync(int orderId);
    Task RefundOrderAsync(int orderId);
}

public interface IOrderQueryService
{
    Task<Order> GetOrderAsync(int id);
    Task<IEnumerable<Order>> GetOrdersAsync();
    Task<OrderStatistics> GetStatisticsAsync(DateTime from, DateTime to);
}

public interface IOrderNotificationService
{
    Task SendOrderConfirmationAsync(int orderId);
}

public interface IOrderDocumentService
{
    Task<Invoice> GenerateInvoiceAsync(int orderId);
}

// Có thể implement riêng biệt
public class OrderService : IOrderCommandService, IOrderQueryService
{
    // Implement các methods
}

// Hoặc tách hoàn toàn (CQRS)
public class OrderCommandService : IOrderCommandService { }
public class OrderQueryService : IOrderQueryService { }

5. Dependency Inversion Principle (DIP)

Định nghĩa

  1. Các module cấp cao không nên phụ thuộc vào module cấp thấp. Cả hai nên phụ thuộc vào abstraction.
  2. Abstraction không nên phụ thuộc vào chi tiết. Chi tiết nên phụ thuộc vào abstraction.

Tại sao cần DIP?

  • Giảm coupling giữa các components
  • Dễ dàng test với mock objects
  • Linh hoạt thay đổi implementation
  • Tuân thủ Dependency Injection

Ví dụ vi phạm DIP

// ❌ VI PHẠM - High-level module phụ thuộc vào low-level module
public class OrderService
{
    private readonly SqlServerOrderRepository _repository;
    private readonly SmtpEmailService _emailService;
    private readonly FileSystemLogger _logger;
    
    public OrderService()
    {
        _repository = new SqlServerOrderRepository();
        _emailService = new SmtpEmailService();
        _logger = new FileSystemLogger();
    }
    
    public void ProcessOrder(Order order)
    {
        _repository.Save(order);
        _emailService.SendConfirmation(order);
        _logger.Log($"Order {order.Id} processed");
    }
}

// Low-level modules
public class SqlServerOrderRepository
{
    public void Save(Order order) { /* SQL Server logic */ }
}

public class SmtpEmailService
{
    public void SendConfirmation(Order order) { /* SMTP logic */ }
}

public class FileSystemLogger
{
    public void Log(string message) { /* File system logic */ }
}

Ví dụ tuân thủ DIP

// ✅ TUÂN THỦ - Cả high-level và low-level phụ thuộc vào abstraction
public interface IOrderRepository
{
    Task SaveAsync(Order order);
}

public interface IEmailService
{
    Task SendConfirmationAsync(Order order);
}

public interface ILogger
{
    Task LogAsync(string message);
}

// High-level module
public class OrderService
{
    private readonly IOrderRepository _repository;
    private readonly IEmailService _emailService;
    private readonly ILogger _logger;
    
    public OrderService(
        IOrderRepository repository,
        IEmailService emailService,
        ILogger logger)
    {
        _repository = repository;
        _emailService = emailService;
        _logger = logger;
    }
    
    public async Task ProcessOrderAsync(Order order)
    {
        await _repository.SaveAsync(order);
        await _emailService.SendConfirmationAsync(order);
        await _logger.LogAsync($"Order {order.Id} processed");
    }
}

// Low-level modules phụ thuộc vào abstraction
public class SqlServerOrderRepository : IOrderRepository
{
    public async Task SaveAsync(Order order)
    {
        // SQL Server implementation
    }
}

public class SmtpEmailService : IEmailService
{
    public async Task SendConfirmationAsync(Order order)
    {
        // SMTP implementation
    }
}

public class FileSystemLogger : ILogger
{
    public async Task LogAsync(string message)
    {
        // File system implementation
    }
}

// Có thể dễ dàng thay thế implementation
public class MongoDbOrderRepository : IOrderRepository
{
    public async Task SaveAsync(Order order)
    {
        // MongoDB implementation
    }
}

public class SendGridEmailService : IEmailService
{
    public async Task SendConfirmationAsync(Order order)
    {
        // SendGrid implementation
    }
}

public class CloudLogger : ILogger
{
    public async Task LogAsync(string message)
    {
        // Cloud logging implementation
    }
}

DIP với Dependency Injection trong ASP.NET Core

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Đăng ký dependencies
builder.Services.AddScoped<IOrderRepository, SqlServerOrderRepository>();
builder.Services.AddScoped<IEmailService, SendGridEmailService>();
builder.Services.AddScoped<ILogger, CloudLogger>();
builder.Services.AddScoped<IOrderService, OrderService>();

// Controller sử dụng DI
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    private readonly IOrderService _orderService;
    
    public OrdersController(IOrderService orderService)
    {
        _orderService = orderService;
    }
    
    [HttpPost]
    public async Task<IActionResult> CreateOrder([FromBody] CreateOrderDto dto)
    {
        var order = await _orderService.CreateOrderAsync(dto);
        return CreatedAtAction(nameof(GetOrder), new { id = order.Id }, order);
    }
}

// Factory Pattern với DI
public interface IReportGenerator
{
    Task<Report> GenerateAsync(ReportData data);
}

public class PdfReportGenerator : IReportGenerator { /* PDF logic */ }
public class ExcelReportGenerator : IReportGenerator { /* Excel logic */ }
public class HtmlReportGenerator : IReportGenerator { /* HTML logic */ }

public class ReportService
{
    private readonly IServiceProvider _serviceProvider;
    
    public ReportService(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    
    public async Task<Report> GenerateReportAsync(ReportType type, ReportData data)
    {
        var generator = type switch
        {
            ReportType.Pdf => _serviceProvider.GetRequiredService<PdfReportGenerator>(),
            ReportType.Excel => _serviceProvider.GetRequiredService<ExcelReportGenerator>(),
            ReportType.Html => _serviceProvider.GetRequiredService<HtmlReportGenerator>(),
            _ => throw new ArgumentException($"Unsupported report type: {type}")
        };
        
        return await generator.GenerateAsync(data);
    }
}

Tổng kết và Best Practices

Khi nào áp dụng SOLID?

  1. SRP: Khi một class đang làm quá nhiều việc, khó test, khó thay đổi
  2. OCP: Khi cần thêm tính năng mới mà không muốn sửa code hiện có
  3. LSP: Khi thiết kế inheritance hierarchy, đặc biệt với abstract class
  4. ISP: Khi interface có quá nhiều methods không liên quan
  5. DIP: Khi muốn giảm coupling, dễ test, dễ thay đổi implementation

Mối quan hệ giữa các nguyên tắc

  • DIP thường dẫn đến ISP (tách interface nhỏ)
  • OCP thường đạt được thông qua DIP (phụ thuộc abstraction)
  • LSP đảm bảo inheritance hierarchy đúng đắn
  • SRP là nền tảng cho tất cả các nguyên tắc khác

Code smells vi phạm SOLID

  1. God Class: Class quá lớn, làm nhiều việc (vi phạm SRP)
  2. Switch Statements: Nhiều switch/case xử lý các type khác nhau (vi phạm OCP)
  3. Empty Method Overrides: Override method để throw exception (vi phạm LSP)
  4. Interface Pollution: Interface với nhiều methods không liên quan (vi phạm ISP)
  5. Concrete Dependencies: Direct instantiation của concrete classes (vi phạm DIP)

Tools hỗ trợ

  1. SonarQube: Phát hiện code smells vi phạm SOLID
  2. ReSharper: Gợi ý refactor để tuân thủ SOLID
  3. Visual Studio Code Metrics: Đo lường maintainability index
  4. Unit Tests: Kiểm tra tính đúng đắn khi refactor

Thực hành trong dự án thực tế

// Ví dụ tổng hợp SOLID trong ASP.NET Core
public interface IRepository<T> : IReadRepository<T>, IWriteRepository<T>
{
    // ISP: Tách read/write
}

public abstract class BaseRepository<T> : IRepository<T>
{
    // DIP: Phụ thuộc DbContext abstraction
    protected readonly DbContext _context;
    
    protected BaseRepository(DbContext context)
    {
        _context = context;
    }
    
    // SRP: Mỗi method làm một việc
    public virtual async Task<T> GetByIdAsync(int id)
    {
        return await _context.Set<T>().FindAsync(id);
    }
    
    // OCP: Virtual method cho phép override
    public virtual async Task AddAsync(T entity)
    {
        await _context.Set<T>().AddAsync(entity);
        await _context.SaveChangesAsync();
    }
}

// LSP: Có thể thay thế bằng specialization
public class AuditableRepository<T> : BaseRepository<T> where T : class, IAuditable
{
    public AuditableRepository(DbContext context) : base(context) { }
    
    public override async Task AddAsync(T entity)
    {
        entity.CreatedAt = DateTime.UtcNow;
        entity.CreatedBy = GetCurrentUserId();
        await base.AddAsync(entity);
    }
}

SOLID không phải là mục tiêu cuối cùng mà là phương tiện để đạt được code chất lượng cao. Áp dụng SOLID đúng cách sẽ tạo ra hệ thống dễ bảo trì, mở rộng và test.

DRY, KISS, YAGNI

Ba nguyên tắc này là nền tảng trong việc viết code clean và maintainable.

DRY - Don’t Repeat Yourself

Concept

Mỗi piece of knowledge trong hệ thống nên có một single, unambiguous representation. Không nên duplicate code hay logic.

Examples

Bad - Code Duplication:

// Duplicate validation logic
public class UserController : ControllerBase
{
    public IActionResult CreateUser(CreateUserRequest request)
    {
        if (string.IsNullOrEmpty(request.Name))
            return BadRequest("Name is required");
            
        if (request.Name.Length < 2)
            return BadRequest("Name must be at least 2 characters");
            
        if (request.Name.Length > 100)
            return BadRequest("Name must not exceed 100 characters");
            
        // Save user
    }
    
    public IActionResult UpdateUser(UpdateUserRequest request)
    {
        // Same validation repeated!
        if (string.IsNullOrEmpty(request.Name))
            return BadRequest("Name is required");
            
        if (request.Name.Length < 2)
            return BadRequest("Name must be at least 2 characters");
            
        if (request.Name.Length > 100)
            return BadRequest("Name must not exceed 100 characters");
            
        // Update user
    }
}

Good - Extract to Shared Method:

public static class ValidationHelper
{
    public static (bool IsValid, string Error) ValidateName(string name)
    {
        if (string.IsNullOrEmpty(name))
            return (false, "Name is required");
            
        if (name.Length < 2)
            return (false, "Name must be at least 2 characters");
            
        if (name.Length > 100)
            return (false, "Name must not exceed 100 characters");
            
        return (true, null);
    }
}

public class UserController : ControllerBase
{
    public IActionResult CreateUser(CreateUserRequest request)
    {
        var (isValid, error) = ValidationHelper.ValidateName(request.Name);
        if (!isValid)
            return BadRequest(error);
            
        // Save user
    }
    
    public IActionResult UpdateUser(UpdateUserRequest request)
    {
        var (isValid, error) = ValidationHelper.ValidateName(request.Name);
        if (!isValid)
            return BadRequest(error);
            
        // Update user
    }
}

Good - Use Inheritance or Composition:

// Base class for shared behavior
public abstract class BaseController : ControllerBase
{
    protected IActionResult ValidateRequest<T>(T request)
    {
        // Common validation
    }
}

public class UserController : BaseController
{
    // Inherits validation
}

Benefits

  • Maintainability: Thay đổi một nơi
  • Reduced bugs: Logic tập trung
  • Better testing: Test một lần
  • Readability: Clearer code

KISS - Keep It Simple, Stupid

Concept

Giải pháp đơn giản nhất thường là tốt nhất. Tránh over-engineering và unnecessary complexity.

Examples

Bad - Over-complicated:

// Too many abstractions
public interface IUserProcessor
{
    Task<IResult<UserProcessingResult>> ProcessAsync(UserProcessingRequest request);
}

public class UserProcessingContext
{
    private readonly IUserProcessor _processor;
    private readonly IUserRepository _repository;
    private readonly ICacheService _cache;
    
    public async Task<IResult<UserProcessingResult>> ProcessUser(
        UserProcessingRequest request,
        ProcessingContext context)
    {
        var strategy = GetProcessingStrategy(context);
        return await strategy.ProcessAsync(request);
    }
}

Good - Simple and Direct:

// Direct and simple
public class UserService
{
    public async Task<User> CreateUser(CreateUserRequest request)
    {
        if (request == null)
            throw new ArgumentNullException(nameof(request));
            
        var user = new User
        {
            Name = request.Name,
            Email = request.Email
        };
        
        await _userRepository.AddAsync(user);
        return user;
    }
}

When to Avoid Over-Simplification

// Don't oversimplify complex domain logic
// Still maintain proper structure for complex scenarios
public class OrderProcessor
{
    private readonly IEnumerable<IOrderValidationStrategy> _strategies;
    
    public OrderProcessor(IEnumerable<IOrderValidationStrategy> strategies)
    {
        _strategies = strategies;
    }
    
    public ValidationResult Validate(Order order)
    {
        var result = new ValidationResult();
        
        foreach (var strategy in _strategies)
        {
            var strategyResult = strategy.Validate(order);
            result.Merge(strategyResult);
        }
        
        return result;
    }
}

Benefits

  • Easier to understand
  • Faster to develop
  • Less bugs
  • Easier to maintain

YAGNI - You Aren’t Gonna Need It

Concept

Không implement tính năng cho đến khi nó thực sự cần thiết. Tránh speculative design.

Examples

Bad - Over-Engineering:

// Building infrastructure for future needs that may never come

public interface IUserRepository
{
    Task<User> GetByIdAsync(Guid id);
    Task<User> GetByEmailAsync(string email);  // Not used yet!
    Task<List<User>> GetByStatusAsync(UserStatus status);  // Not used yet!
    Task<User> GetByPhoneAsync(string phone);  // Not used yet!
    Task<List<User>> SearchAsync(string query);  // Not used yet!
}

public class UserRepository : IUserRepository
{
    // Implementing all these methods before they're needed
}

Good - Just Enough:

public interface IUserRepository
{
    Task<User> GetByIdAsync(Guid id);
    // Add more as needed
}

public class UserRepository : IUserRepository
{
    public async Task<User> GetByIdAsync(Guid id)
    {
        return await _context.Users.FindAsync(id);
    }
}

When You Actually Need Extensibility

// If you genuinely anticipate change, use interface properly
public interface IUserRepository
{
    Task<User> GetByIdAsync(Guid id);
}

// Extensibility built in through interface, not through extra methods
public interface IUserRepository
{
    // Base operations
}

// Future: Easy to extend with decorator pattern
public class CachedUserRepository : IUserRepository
{
    private readonly IUserRepository _inner;
    private readonly ICache _cache;
    
    public async Task<User> GetByIdAsync(Guid id)
    {
        return await _cache.GetOrSetAsync(
            $"user:{id}",
            () => _inner.GetByIdAsync(id));
    }
}

Don’t Confuse YAGNI with Bad Design

// YAGNI doesn't mean "write bad code"
public class PaymentService
{
    // Good: Proper abstraction exists
    private readonly IPaymentGateway _paymentGateway;
    
    // Bad: Hardcoded, not testable
    private readonly SqlConnection _connection;
}

Benefits

  • Faster delivery: Focus on what matters now
  • Less waste: No unused code
  • Simpler codebase: Easier to navigate
  • Adaptability: Change direction easily

How They Work Together

DRY    → Avoid duplication
KISS   → Keep solution simple
YAGNI  → Only build what's needed

Together: Build simple, non-repeated solutions for current needs

Anti-Patterns to Avoid

Anti-PatternDescription
Premature AbstractionCreating abstractions before needed
Speculative GeneralityBuilding for “future” features
Golden HammerUsing one solution for everything
Not Invented HereAvoiding existing solutions

Real-World Application

// Before: DRY violation
public class OrderConfirmationEmail
{
    public void SendConfirmation(Order order)
    {
        var body = $"Order #{order.Id}\n";
        body += $"Customer: {order.CustomerName}\n";
        
        // Duplicate email formatting logic
    }
}

public class ShippingNotificationEmail
{
    public void SendNotification(Order order)
    {
        var body = $"Order #{order.Id}\n";
        body += $"Customer: {order.CustomerName}\n";
        
        // Same logic again!
    }
}

// After: DRY + KISS + YAGNI
public class EmailService
{
    // Shared, simple, just what's needed
    public void Send(string template, object data)
    {
        var body = RenderTemplate(template, data);
        _emailGateway.Send(body);
    }
}

Summary

PrincipleFocusAction
DRYDuplicationExtract shared code
KISSComplexitySimplify solution
YAGNIFutureBuild only what’s needed

References

Separation of Concerns (SoC)

Overview

Separation of Concerns là nguyên tắc thiết kế tách biệt một ứng dụng thành các phần riêng biệt, mỗi phần xử lý một responsibility cụ thể. Mục tiêu là giảm coupling, tăng maintainability, và dễ dàng thay đổi một phần mà không ảnh hưởng đến các phần khác.

Core Concept

Before: Mixed Concerns
┌─────────────────────────────────────────────┐
│         UserController                      │
│  - Handle HTTP requests                     │
│  - Business logic (validation, processing)  │
│  - Data access (queries)                    │
│  - Authentication                           │
│  - Logging                                  │
└─────────────────────────────────────────────┘

After: Separation of Concerns
┌─────────────────────────────────────────────┐
│  UserController    → Handle HTTP requests   │
│  UserService       → Business logic         │
│  UserRepository    → Data access            │
│  AuthService       → Authentication        │
│  Logger            → Logging               │
└─────────────────────────────────────────────┘

Types of Concerns

1. Business Logic Concern

// Business logic separated from other concerns
public class OrderService
{
    public decimal CalculateTotal(Order order, Discount discount)
    {
        // Pure business logic
        var subtotal = order.Items.Sum(i => i.Price * i.Quantity);
        return subtotal * (1 - discount.Percentage);
    }
}

2. Data Access Concern

public interface IOrderRepository
{
    Task<Order> GetByIdAsync(Guid id);
    Task<List<Order>> GetByCustomerIdAsync(Guid customerId);
    Task AddAsync(Order order);
    Task UpdateAsync(Order order);
}

3. Presentation Concern

public class OrdersController : ControllerBase
{
    // Only handles HTTP concerns
    [HttpGet("{id}")]
    public async Task<IActionResult> GetOrder(Guid id)
    {
        var order = await _orderService.GetOrderAsync(id);
        return Ok(order);
    }
}

4. Cross-Cutting Concerns

// Logging, security, validation - apply to multiple layers

// Logging interceptor
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
    private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;
    
    public async Task<TResponse> Handle(TRequest request, 
        RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
    {
        _logger.LogInformation("Handling {RequestType}", typeof(TRequest).Name);
        var response = await next();
        _logger.LogInformation("Handled {RequestType}", typeof(TRequest).Name);
        return response;
    }
}

Implementation Levels

1. Layer Separation

// Presentation Layer
public class ProductsController : ControllerBase
{
    [HttpGet]
    public async Task<IActionResult> GetProducts()
    {
        // Only presentation logic
    }
}

// Application Layer
public class ProductService
{
    // Business logic
}

// Infrastructure Layer
public class ProductRepository
{
    // Data access
}

2. Class Level

// Single Responsibility Principle (SRP)
public class EmailService
{
    public void SendConfirmation(string email, Order order)
    {
        // Single responsibility: sending order confirmations
    }
}

public class OrderService
{
    // Different service for order operations
}

3. Method Level

public class OrderProcessor
{
    public void Process(Order order)
    {
        ValidateOrder(order);      // Validation concern
        CalculateTotal(order);     // Calculation concern
        SaveOrder(order);          // Persistence concern
        NotifyCustomer(order);     // Notification concern
    }
    
    // Each method handles one concern
}

Benefits

BenefitDescription
MaintainabilityChange one concern without affecting others
TestabilityTest each concern in isolation
ReusabilityReuse concerns across applications
Parallel DevelopmentDifferent teams work on different concerns
FlexibilityEasy to change implementation

Example: E-commerce Application

┌─────────────────────────────────────────────────────────────┐
│                    Architecture Layers                       │
├─────────────────────────────────────────────────────────────┤
│  Presentation (API)     │ Controllers, DTOs, Validators   │
├─────────────────────────────────────────────────────────────┤
│  Application (Use Cases)│ Services, Commands, Queries     │
├─────────────────────────────────────────────────────────────┤
│  Domain (Business Logic)│ Entities, Value Objects, Rules  │
├─────────────────────────────────────────────────────────────┤
│  Infrastructure        │ Repositories, External APIs      │
├─────────────────────────────────────────────────────────────┤
│  Cross-Cutting         │ Logging, Security, Caching       │
└─────────────────────────────────────────────────────────────┘

Implementation in ASP.NET Core

// Program.cs - Setting up concern separation
var builder = WebApplication.CreateBuilder(args);

// Services with clear separation
builder.Services.AddControllers();           // Presentation
builder.Services.AddScoped<IOrderService, OrderService>();  // Application
builder.Services.AddScoped<IOrderRepository, OrderRepository>();  // Infrastructure

// Cross-cutting concerns
builder.Services.AddLogging();               // Logging
builder.Services.AddAuthentication();        // Security
builder.Services.AddMemoryCache();           // Caching
builder.Services.AddAutoMapper();             // Mapping

Anti-Patterns to Avoid

// God Object - handles too many concerns
public class GodClass
{
    public void HandleHttpRequest() { }      // Presentation
    public void ValidateInput() { }          // Validation
    public void ProcessBusiness() { }        // Business
    public void SaveToDatabase() { }          // Data
    public void LogOperation() { }           // Logging
}

// Spaghetti Code - tangled concerns
public class RandomClass
{
    public void DoEverything() { } // Everything mixed together
}

Best Practices

  1. Identify Clear Boundaries: Define what each concern is responsible for
  2. Use Interfaces: Depend on abstractions, not concrete implementations
  3. Minimize Dependencies: Only depend on concerns you need
  4. Cohesion: Keep related code together
  5. Consistency: Apply same separation across entire codebase

Design Patterns

Overview

Design patterns là các giải pháp được kiểm chứng cho các vấn đề thiết kế phần mềm thường gặp. Chúng cung cấp template cho việc giải quyết vấn đề có thể được áp dụng trong nhiều tình huống khác nhau.

Categories

Design patterns được chia thành 3 nhóm chính:

1. Creational Patterns

Những pattern liên quan đến việc khởi tạo object, giúp tách biệt quá trình khởi tạo khỏi logic nghiệp vụ.

  • Singleton: Đảm bảo chỉ có một instance của class
  • Factory Method: Định nghĩa interface cho việc tạo object
  • Abstract Factory: Tạo families of related objects
  • Builder: Tách biệt construction của complex object
  • Prototype: Tạo object bằng cách cloning một object khác

2. Structural Patterns

Những pattern liên quan đến cách kết hợp objects và classes để tạo thành cấu trúc lớn hơn.

  • Adapter: Chuyển đổi interface của một class sang interface khác
  • Bridge: Tách abstraction khỏi implementation
  • Composite: Tạo cấu trúc tree-like
  • Decorator: Thêm behavior vào object dynamically
  • Facade: Cung cấp simplified interface cho complex subsystem
  • Flyweight: Chia sẻ objects để tiết kiệm memory
  • Proxy: Cung cấp placeholder cho another object

3. Behavioral Patterns

Những pattern liên quan đến communication giữa objects và assignment of responsibilities.

  • Chain of Responsibility: Pass request along a chain of handlers
  • Command: Encapsulate request as an object
  • Iterator: Truy cập elements của một collection sequentially
  • Mediator: Định nghĩa communication giữa objects
  • Memento: Capture and externalize object’s state
  • Observer: Define one-to-many dependency
  • State: Thay đổi behavior khi object’s state thay đổi
  • Strategy: Định nghĩa family of algorithms
  • Template Method: Định nghĩa skeleton của algorithm
  • Visitor: Định nghĩa operation trên elements của một object structure

When to Use

Sử dụng design patterns khi:

  • Gặp vấn đề thiết kế lặp đi lặp lại
  • Cần solution đã được kiểm chứng
  • Muốn improve communication trong team bằng common vocabulary

Anti-Patterns

Tránh sử dụng pattern không phù hợp:

  • Over-engineering: Áp dụng quá nhiều patterns
  • Golden Hammer: Muốn apply một pattern cho mọi vấn đề
  • Cargo Cult: Sử dụng pattern vì thấy người khác dùng, không hiểu tại sao

Reference

Xem chi tiết từng pattern:

Creational Patterns

Creational patterns tập trung vào việc khởi tạo objects, giúp tách biệt quá trình khởi tạo khỏi logic nghiệp vụ và tạo objects một cách linh hoạt hơn.

1. Singleton Pattern

Mục đích: Đảm bảo một class chỉ có một instance duy nhất và cung cấp global access point đến instance đó.

C# Implementation

public sealed class Singleton
{
    private static readonly Lazy<Singleton> _instance = 
        new Lazy<Singleton>(() => new Singleton());
    
    public static Singleton Instance => _instance.Value;
    
    private Singleton()
    {
        // Private constructor prevents instantiation
    }
}

Use Cases

  • Database connections
  • Logging services
  • Configuration managers
  • Caching services

Considerations

  • Thread-safety với Lazy<T>
  • Consider DI container thay vì Singleton
  • IDisposable nếu cần cleanup resources

2. Factory Method Pattern

Mục đích: Định nghĩa interface để tạo object, nhưng để subclasses quyết định class nào sẽ được khởi tạo.

C# Implementation

public interface IProduct
{
    string Operation();
}

public abstract class Creator
{
    public abstract IProduct FactoryMethod();
    
    public string SomeOperation()
    {
        var product = FactoryMethod();
        return product.Operation();
    }
}

public class ConcreteProductA : IProduct
{
    public string Operation() => "Result of ConcreteProductA";
}

public class ConcreteCreatorA : Creator
{
    public override IProduct FactoryMethod() => new ConcreteProductA();
}

Use Cases

  • UI frameworks (create different controls)
  • Document generators (PDF, Word, etc.)
  • Payment processors

3. Abstract Factory Pattern

Mục đích: Tạo families of related objects mà không cần specify concrete classes.

C# Implementation

public interface IButton
{
    void Render();
}

public interface ICheckbox
{
    void Render();
}

// Abstract Factory
public interface IUIFactory
{
    IButton CreateButton();
    ICheckbox CreateCheckbox();
}

public class WindowsFactory : IUIFactory
{
    public IButton CreateButton() => new WindowsButton();
    public ICheckbox CreateCheckbox() => new WindowsCheckbox();
}

public class MacFactory : IUIFactory
{
    public IButton CreateButton() => new MacButton();
    public ICheckbox CreateCheckbox() => new MacCheckbox();
}

Use Cases

  • Cross-platform applications
  • Theme systems
  • Database abstraction (SQL Server, PostgreSQL, etc.)

4. Builder Pattern

Mục đích: Tách biệt construction của complex object khỏi representation, cho phép tạo different representations.

C# Implementation

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Address { get; set; }
}

public class PersonBuilder
{
    private readonly Person _person = new Person();
    
    public PersonBuilder WithName(string name)
    {
        _person.Name = name;
        return this;
    }
    
    public PersonBuilder WithAge(int age)
    {
        _person.Age = age;
        return this;
    }
    
    public PersonBuilder WithAddress(string address)
    {
        _person.Address = address;
        return this;
    }
    
    public Person Build() => _person;
}

// Usage
var person = new PersonBuilder()
    .WithName("John")
    .WithAge(30)
    .WithAddress("123 Main St")
    .Build();

Use Cases

  • Building complex objects (SQL queries, HTTP requests)
  • Immutable objects
  • Fluent APIs
  • Object with many optional parameters

5. Prototype Pattern

Mục đích: Tạo new objects bằng cách cloning một existing object (prototype).

C# Implementation

public interface IPrototype<T>
{
    T Clone();
}

public class Person : IPrototype<Person>
{
    public string Name { get; set; }
    public Address Address { get; set; }
    
    public Person Clone()
    {
        // Shallow copy
        return (Person)this.MemberwiseClone();
        
        // Or deep copy
        return new Person
        {
            Name = this.Name,
            Address = new Address 
            { 
                Street = this.Address.Street,
                City = this.Address.City
            }
        };
    }
}

Use Cases

  • Complex object creation that’s expensive
  • Object caching
  • Avoiding subclassing

Comparison

PatternPurposeComplexity
SingletonSingle instanceLow
Factory MethodSubclass decidesMedium
Abstract FactoryFamilies of objectsHigh
BuilderComplex constructionMedium
PrototypeCloning objectsLow-Medium

Best Practices

  1. Chọn đúng pattern: Cân nhắc requirements trước khi áp dụng
  2. Prefer Composition over Inheritance: Builder và Factory thường tốt hơn inheritance
  3. Consider DI Containers: Trong modern .NET, DI thường replace Singleton pattern
  4. Immutability: Sử dụng Builder cho immutable objects
  5. Thread Safety: Đặc biệt quan trọng với Singleton trong multi-threaded applications

Structural Patterns

Structural patterns tập trung vào cách kết hợp objects và classes để tạo thành cấu trúc lớn hơn, đảm bảo tính linh hoạt và hiệu quả trong thiết kế hệ thống.

1. Adapter Pattern

Mục đích: Cho phép interface của một class không tương thích hoạt động với interface khác.

C# Implementation

// Target interface
public interface IMediaPlayer
{
    void Play(string filename);
}

// Adaptee - existing class with incompatible interface
public class AdvancedMediaPlayer
{
    public void PlayMp4(string filename)
    {
        Console.WriteLine($"Playing MP4: {filename}");
    }
    
    public void PlayVlc(string filename)
    {
        Console.WriteLine($"Playing VLC: {filename}");
    }
}

// Adapter
public class MediaAdapter : IMediaPlayer
{
    private readonly AdvancedMediaPlayer _advancedPlayer;
    
    public MediaAdapter()
    {
        _advancedPlayer = new AdvancedMediaPlayer();
    }
    
    public void Play(string filename)
    {
        if (filename.EndsWith(".mp4"))
        {
            _advancedPlayer.PlayMp4(filename);
        }
        else if (filename.EndsWith(".vlc"))
        {
            _advancedPlayer.PlayVlc(filename);
        }
    }
}

Use Cases

  • Integrating legacy systems với modern code
  • Working with third-party libraries
  • Converting between different data formats

2. Bridge Pattern

Mục đích: Tách abstraction khỏi implementation để cả hai có thể thay đổi độc lập.

C# Implementation

// Implementation interface
public interface IRenderer
{
    void RenderCircle(float radius);
    void RenderSquare(float side);
}

// Concrete implementations
public class VectorRenderer : IRenderer
{
    public void RenderCircle(float radius) 
        => Console.WriteLine($"Drawing circle as vectors with radius {radius}");
    public void RenderSquare(float side) 
        => Console.WriteLine($"Drawing square as vectors with side {side}");
}

public class RasterRenderer : IRenderer
{
    public void RenderCircle(float radius) 
        => Console.WriteLine($"Drawing circle as pixels with radius {radius}");
    public void RenderSquare(float side) 
        => Console.WriteLine($"Drawing square as pixels with side {side}");
}

// Abstraction
public abstract class Shape
{
    protected IRenderer Renderer;
    
    protected Shape(IRenderer renderer) => Renderer = renderer;
    
    public abstract void Draw();
    public abstract void Resize(float factor);
}

public class Circle : Shape
{
    private float _radius;
    
    public Circle(IRenderer renderer, float radius) : base(renderer)
    {
        _radius = radius;
    }
    
    public override void Draw() => Renderer.RenderCircle(_radius);
    public override void Resize(float factor) => _radius *= factor;
}

Use Cases

  • Cross-platform applications
  • Multiple UI frameworks
  • Database drivers

3. Composite Pattern

Mục đích: Compose objects into tree structures để represent part-whole hierarchies.

C# Implementation

public interface IComponent
{
    void Execute();
    int GetPrice();
}

public class Leaf : IComponent
{
    private readonly int _price;
    private readonly string _name;
    
    public Leaf(string name, int price)
    {
        _name = name;
        _price = price;
    }
    
    public void Execute() => Console.WriteLine($"Leaf: {_name}");
    public int GetPrice() => _price;
}

public class Composite : IComponent
{
    private readonly List<IComponent> _children = new();
    
    public void Add(IComponent component) => _children.Add(component);
    public void Remove(IComponent component) => _children.Remove(component);
    
    public void Execute()
    {
        foreach (var child in _children)
            child.Execute();
    }
    
    public int GetPrice() => _children.Sum(c => c.GetPrice());
}

Use Cases

  • File systems
  • UI hierarchies
  • Organization structures

4. Decorator Pattern

Mục đích: Thêm behavior vào objects dynamically mà không thay đổi class gốc.

C# Implementation

public interface ICoffee
{
    string GetDescription();
    decimal GetCost();
}

public class SimpleCoffee : ICoffee
{
    public string GetDescription() => "Simple Coffee";
    public decimal GetCost() => 2.00m;
}

// Base decorator
public abstract class CoffeeDecorator : ICoffee
{
    protected ICoffee _coffee;
    
    public CoffeeDecorator(ICoffee coffee) => _coffee = coffee;
    
    public virtual string GetDescription() => _coffee.GetDescription();
    public virtual decimal GetCost() => _coffee.GetCost();
}

public class MilkDecorator : CoffeeDecorator
{
    public MilkDecorator(ICoffee coffee) : base(coffee) { }
    
    public override string GetDescription() 
        => _coffee.GetDescription() + ", Milk";
    
    public override decimal GetCost() => _coffee.GetCost() + 0.50m;
}

public class SugarDecorator : CoffeeDecorator
{
    public SugarDecorator(ICoffee coffee) : base(coffee) { }
    
    public override string GetDescription() 
        => _coffee.GetDescription() + ", Sugar";
    
    public override decimal GetCost() => _coffee.GetCost() + 0.25m;
}

// Usage
ICoffee coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);

Use Cases

  • Adding features to streams (buffering, compression)
  • UI components với borders, scrollbars
  • Logging, caching decorators

5. Facade Pattern

Mục đích: Cung cấp simplified interface cho complex subsystem.

C# Implementation

public class VideoFile
{
    public string Filename { get; }
    public string CodecType { get; }
    
    public VideoFile(string filename)
    {
        Filename = filename;
        CodecType = filename.EndsWith(".mp4") ? "mp4" : "ogg";
    }
}

public class CodecFactory
{
    public static string Extract(VideoFile file) => file.CodecType;
}

public class BitrateReader
{
    public static string Read(string codec) => "video data";
}

public class AudioMixer
{
    public string Fix(string data) => "fixed audio";
}

public class VideoConverter
{
    public string Convert(string filename, string format)
    {
        var file = new VideoFile(filename);
        var codec = CodecFactory.Extract(file);
        var data = BitrateReader.Read(codec);
        var audio = new AudioMixer().Fix(data);
        
        return "converted file";
    }
}

Use Cases

  • Simplifying library APIs
  • Hiding complexity của third-party systems
  • Providing unified interface to multiple services

6. Flyweight Pattern

Mục đích: Chia sẻ objects để tiết kiệm memory, đặc biệt khi có nhiều objects giống nhau.

C# Implementation

public class TreeType
{
    public string Name { get; }
    public string Color { get; }
    public string Texture { get; }
    
    public TreeType(string name, string color, string texture)
    {
        Name = name;
        Color = color;
        Texture = texture;
    }
    
    public void Draw(int x, int y)
    {
        Console.DrawTree(x, y, Color, Texture);
    }
}

public class TreeFactory
{
    private static readonly Dictionary<string, TreeType> _cache = new();
    
    public static TreeType GetTreeType(string name, string color, string texture)
    {
        var key = $"{name}_{color}_{texture}";
        
        if (!_cache.ContainsKey(key))
        {
            _cache[key] = new TreeType(name, color, texture);
        }
        
        return _cache[key];
    }
}

public class Tree
{
    private readonly int _x, _y;
    private readonly TreeType _type;
    
    public Tree(int x, int y, TreeType type)
    {
        _x = x;
        _y = y;
        _type = type;
    }
    
    public void Draw() => _type.Draw(_x, _y);
}

Use Cases

  • Game development (trees, particles)
  • Caching shared resources
  • Reducing memory usage với large number of similar objects

7. Proxy Pattern

Mục đích: Cung cấp placeholder cho another object để control access to it.

C# Implementation

public interface IImage
{
    void Display();
}

public class RealImage : IImage
{
    private readonly string _filename;
    
    public RealImage(string filename)
    {
        _filename = filename;
        LoadFromDisk();
    }
    
    private void LoadFromDisk()
    {
        Console.WriteLine($"Loading image: {_filename}");
    }
    
    public void Display()
    {
        Console.WriteLine($"Displaying image: {_filename}");
    }
}

public class ProxyImage : IImage
{
    private readonly string _filename;
    private RealImage _realImage;
    
    public ProxyImage(string filename)
    {
        _filename = filename;
    }
    
    public void Display()
    {
        if (_realImage == null)
        {
            _realImage = new RealImage(_filename);
        }
        _realImage.Display();
    }
}

Use Cases

  • Lazy loading
  • Access control
  • Logging và monitoring
  • Caching

Comparison

PatternPurposeUse Case
AdapterConvert interfaceLegacy integration
BridgeSeparate abstraction/implementationCross-platform
CompositeTree structureUI, file systems
DecoratorAdd behavior dynamicallyFeature extension
FacadeSimplify interfaceComplex subsystems
FlyweightShare objectsMemory optimization
ProxyControl accessLazy loading, security

Best Practices

  1. Adapter: Use when integrating incompatible interfaces
  2. Bridge: Use when you have multiple platforms/formats
  3. Composite: Use when representing hierarchical structures
  4. Decorator: Use when you need flexible behavior extension
  5. Facade: Use to simplify complex systems
  6. Flyweight: Use when you have many similar objects
  7. Proxy: Use for lazy loading và access control

Behavioral Patterns

Behavioral patterns tập trung vào việc giao tiếp giữa objects và cách assign responsibilities cho objects, giúp code dễ maintain và mở rộng hơn.

1. Observer Pattern

Mục đích: Định nghĩa one-to-many dependency giữa objects, khi một object thay đổi, tất cả dependents được notify.

C# Implementation

public interface IObserver
{
    void Update(string message);
}

public interface ISubject
{
    void Attach(IObserver observer);
    void Detach(IObserver observer);
    void Notify();
}

public class Subject : ISubject
{
    private readonly List<IObserver> _observers = new();
    private string _state;
    
    public string State
    {
        get => _state;
        set
        {
            _state = value;
            Notify();
        }
    }
    
    public void Attach(IObserver observer) => _observers.Add(observer);
    public void Detach(IObserver observer) => _observers.Remove(observer);
    public void Notify()
    {
        foreach (var observer in _observers)
        {
            observer.Update(_state);
        }
    }
}

public class ConcreteObserver : IObserver
{
    private readonly string _name;
    
    public ConcreteObserver(string name) => _name = name;
    
    public void Update(string message)
    {
        Console.WriteLine($"{_name} received: {message}");
    }
}

Use Cases

  • Event systems
  • UI data binding
  • Real-time updates
  • Logging và monitoring

2. Strategy Pattern

Mục đích: Định nghĩa family of algorithms, encapsulate mỗi algorithm, và làm chúng interchangeable.

C# Implementation

public interface ISortStrategy<T>
{
    void Sort(List<T> items);
}

public class BubbleSortStrategy<T> : ISortStrategy<T> where T : IComparable<T>
{
    public void Sort(List<T> items)
    {
        // Bubble sort implementation
        Console.WriteLine("Sorting using Bubble Sort");
    }
}

public class QuickSortStrategy<T> : ISortStrategy<T> where T : IComparable<T>
{
    public void Sort(List<T> items)
    {
        // Quick sort implementation
        Console.WriteLine("Sorting using Quick Sort");
    }
}

public class Sorter<T>
{
    private ISortStrategy<T> _strategy;
    
    public Sorter(ISortStrategy<T> strategy) => _strategy = strategy;
    
    public void SetStrategy(ISortStrategy<T> strategy) => _strategy = strategy;
    
    public void Sort(List<T> items) => _strategy.Sort(items);
}

Use Cases

  • Payment processing (different payment methods)
  • Compression algorithms
  • Authentication strategies
  • Sorting và searching algorithms

3. Command Pattern

Mục đích: Encapsulate request as an object, cho phép parameterize và queue requests.

C# Implementation

public interface ICommand
{
    void Execute();
    void Undo();
}

public class Light
{
    public void On() => Console.WriteLine("Light is ON");
    public void Off() => Console.WriteLine("Light is OFF");
}

public class LightOnCommand : ICommand
{
    private readonly Light _light;
    
    public LightOnCommand(Light light) => _light = light;
    
    public void Execute() => _light.On();
    public void Undo() => _light.Off();
}

public class RemoteControl
{
    private ICommand _command;
    
    public void SetCommand(ICommand command) => _command = command;
    
    public void PressButton() => _command.Execute();
    public void PressUndo() => _command.Undo();
}

Use Cases

  • Undo/Redo functionality
  • Transaction management
  • Task scheduling
  • Macro recording

4. State Pattern

Mục đích: Cho phép object thay đổi behavior khi internal state thay đổi.

C# Implementation

public interface IState
{
    void InsertCoin(VendingMachine machine);
    void SelectProduct(VendingMachine machine);
    public string GetStateName();
}

public class VendingMachine
{
    public IState CurrentState { get; set; }
    public int CoinCount { get; private set; }
    
    public VendingMachine()
    {
        CurrentState = new NoCoinState();
    }
    
    public void InsertCoin()
    {
        CurrentState.InsertCoin(this);
    }
    
    public void SelectProduct()
    {
        CurrentState.SelectProduct(this);
    }
}

public class NoCoinState : IState
{
    public void InsertCoin(VendingMachine machine)
    {
        machine.CoinCount++;
        machine.CurrentState = new HasCoinState();
        Console.WriteLine("Coin inserted");
    }
    
    public void SelectProduct(VendingMachine machine)
    {
        Console.WriteLine("Please insert coin first");
    }
}

public class HasCoinState : IState
{
    public void InsertCoin(VendingMachine machine)
    {
        Console.WriteLine("Coin already inserted");
    }
    
    public void SelectProduct(VendingMachine machine)
    {
        Console.WriteLine("Product dispensed");
        machine.CurrentState = new NoCoinState();
    }
}

Use Cases

  • Order processing workflows
  • Game state management
  • Document approval workflows
  • TCP connection states

5. Template Method Pattern

Mục đích: Định nghĩa skeleton của algorithm, để subclasses override specific steps.

C# Implementation

public abstract class DataMiner
{
    // Template method
    public void Mine(string path)
    {
        var file = OpenFile(path);
        var data = ExtractData(file);
        var parsed = ParseData(data);
        var analysis = AnalyzeData(parsed);
        SendReport(analysis);
        CloseFile(file);
    }
    
    protected abstract object ExtractData(object file);
    protected abstract object ParseData(object data);
    protected abstract string AnalyzeData(object parsed);
    
    protected virtual object OpenFile(string path) 
    {
        Console.WriteLine($"Opening file: {path}");
        return new object();
    }
    
    protected virtual void CloseFile(object file)
    {
        Console.WriteLine("Closing file");
    }
    
    protected virtual void SendReport(string analysis)
    {
        Console.WriteLine($"Report: {analysis}");
    }
}

public class PDFDataMiner : DataMiner
{
    protected override object ExtractData(object file)
    {
        Console.WriteLine("Extracting PDF data");
        return new object();
    }
    
    protected override object ParseData(object data)
    {
        Console.WriteLine("Parsing PDF data");
        return new object();
    }
    
    protected override string AnalyzeData(object parsed)
    {
        return "PDF Analysis Result";
    }
}

Use Cases

  • Data processing pipelines
  • Build processes
  • Test frameworks
  • Data import/export

6. Chain of Responsibility Pattern

Mục đích: Pass request along a chain of handlers, mỗi handler decide xử lý request hoặc pass tiếp.

C# Implementation

public abstract class Handler
{
    private Handler _nextHandler;
    
    public Handler SetNext(Handler handler)
    {
        _nextHandler = handler;
        return handler;
    }
    
    public void HandleRequest(Request request)
    {
        if (CanHandle(request))
        {
            Process(request);
        }
        else if (_nextHandler != null)
        {
            _nextHandler.HandleRequest(request);
        }
    }
    
    protected abstract bool CanHandle(Request request);
    protected abstract void Process(Request request);
}

public class AuthHandler : Handler
{
    protected override bool CanHandle(Request request) 
        => request.RequiresAuth;
    
    protected override void Process(Request request)
    {
        Console.WriteLine("Authentication successful");
    }
}

public class ValidationHandler : Handler
{
    protected override bool CanHandle(Request request) 
        => request.RequiresValidation;
    
    protected override void Process(Request request)
    {
        Console.WriteLine("Validation successful");
    }
}

Use Cases

  • Authentication/Authorization pipelines
  • Middleware in web frameworks
  • Event handling systems
  • Logging levels

7. Iterator Pattern

Mục đích: Truy cập elements của một collection sequentially mà không expose underlying representation.

C# Implementation

public interface IIterator<T>
{
    bool HasNext();
    T Next();
    void Reset();
}

public interface IEnumerable<T>
{
    IIterator<T> GetEnumerator();
}

public class BookCollection : IEnumerable<string>
{
    private readonly List<string> _books = new();
    
    public void AddBook(string book) => _books.Add(book);
    
    public IIterator<string> GetEnumerator()
    {
        return new BookIterator(_books);
    }
}

public class BookIterator : IIterator<string>
{
    private readonly List<string> _books;
    private int _position;
    
    public BookIterator(List<string> books)
    {
        _books = books;
    }
    
    public bool HasNext() => _position < _books.Count;
    
    public string Next() => _books[_position++];
    
    public void Reset() => _position = 0;
}

Use Cases

  • Collections traversal
  • Database result sets
  • File processing
  • Tree/graph traversal

8. Mediator Pattern

Mục đích: Định nghĩa object encapsulates how a set of objects interact.

C# Implementation

public interface IChatMediator
{
    void SendMessage(string message, User sender);
    void AddUser(User user);
}

public class ChatRoom : IChatMediator
{
    private readonly List<User> _users = new();
    
    public void AddUser(User user) => _users.Add(user);
    
    public void SendMessage(string message, User sender)
    {
        foreach (var user in _users)
        {
            if (user != sender)
            {
                user.Receive(message, sender.Name);
            }
        }
    }
}

public class User
{
    public string Name { get; }
    private readonly IChatMediator _mediator;
    
    public User(string name, IChatMediator mediator)
    {
        Name = name;
        _mediator = mediator;
    }
    
    public void Send(string message) => _mediator.SendMessage(message, this);
    
    public void Receive(string message, string from)
    {
        Console.WriteLine($"{from}: {message}");
    }
}

Use Cases

  • Chat applications
  • UI event handling
  • Air traffic control
  • Event aggregation

9. Memento Pattern

Mục đích: Capture và externalize object’s internal state để restore later.

C# Implementation

public class EditorMemento
{
    public string Content { get; }
    public int CursorPosition { get; }
    public DateTime Timestamp { get; }
    
    public EditorMemento(string content, int cursorPosition)
    {
        Content = content;
        CursorPosition = cursorPosition;
        Timestamp = DateTime.Now;
    }
}

public class Editor
{
    public string Content { get; private set; }
    public int CursorPosition { get; private set; }
    
    private readonly Stack<EditorMemento> _history = new();
    
    public void Type(string text)
    {
        Content = Content.Insert(CursorPosition, text);
        CursorPosition += text.Length;
    }
    
    public EditorMemento Save()
    {
        return new EditorMemento(Content, CursorPosition);
    }
    
    public void Restore(EditorMemento memento)
    {
        Content = memento.Content;
        CursorPosition = memento.CursorPosition;
    }
}

Use Cases

  • Undo/Redo functionality
  • Transaction rollback
  • Checkpoints in games
  • State snapshots

10. Visitor Pattern

Mục đích: Định nghĩa operation trên elements của một object structure without changing classes.

C# Implementation

public interface IElement
{
    void Accept(IVisitor visitor);
}

public class Book : IElement
{
    public string Title { get; }
    public decimal Price { get; }
    
    public Book(string title, decimal price)
    {
        Title = title;
        Price = price;
    }
    
    public void Accept(IVisitor visitor) => visitor.VisitBook(this);
}

public interface IVisitor
{
    void VisitBook(Book book);
    void VisitMagazine(Magazine magazine);
}

public class PriceCalculator : IVisitor
{
    public decimal Total { get; private set; }
    
    public void VisitBook(Book book)
    {
        Total += book.Price;
        Console.WriteLine($"Book: {book.Title} - ${book.Price}");
    }
    
    public void VisitMagazine(Magazine magazine)
    {
        Total += magazine.Price;
        Console.WriteLine($"Magazine: {magazine.Title} - ${magazine.Price}");
    }
}

Use Cases

  • Report generation
  • File system operations
  • Tax calculation
  • Expression tree evaluation

Comparison

PatternPurposeComplexity
ObserverEvent notificationsLow
StrategyInterchangeable algorithmsLow
CommandEncapsulated requestsMedium
StateObject state behaviorMedium
Template MethodAlgorithm skeletonLow
Chain of ResponsibilitySequential handlersMedium
IteratorCollection traversalLow
MediatorObject communicationMedium
MementoState restorationLow
VisitorOperations on structuresMedium

Best Practices

  1. Observer: Use for loose coupling, event-driven systems
  2. Strategy: Use when you need multiple algorithms
  3. Command: Use for undo, queuing, transactions
  4. State: Use for state machines
  5. Template Method: Use for algorithm frameworks
  6. Chain: Use for processing pipelines
  7. Iterator: Use for custom traversal
  8. Mediator: Use for reducing dependencies
  9. Memento: Use for state snapshots
  10. Visitor: Use for operations on complex structures

Kiến trúc Ứng dụng

Clean Architecture

Layer Structure

┌─────────────────────────────────────────────────────────────────┐
│                      PRESENTATION LAYER                         │
│   (Controllers, Views, DTOs, ViewModels)                       │
├─────────────────────────────────────────────────────────────────┤
│                      APPLICATION LAYER                          │
│   (Use Cases, Commands, Queries, Services, DTOs)                │
├─────────────────────────────────────────────────────────────────┤
│                        DOMAIN LAYER                             │
│   (Entities, Value Objects, Domain Services, Interfaces)       │
├─────────────────────────────────────────────────────────────────┤
│                     INFRASTRUCTURE LAYER                        │
│   (DbContext, Repositories, External Services, Logging)         │
└─────────────────────────────────────────────────────────────────┘

Project Structure

src/
├── Domain/
│   ├── Entities/
│   │   └── Product.cs
│   ├── ValueObjects/
│   │   └── Money.cs
│   ├── Interfaces/
│   │   ├── IProductRepository.cs
│   │   └── IEmailService.cs
│   └── Services/
│       └── DomainProductService.cs
│
├── Application/
│   ├── Commands/
│   │   ├── CreateProductCommand.cs
│   │   └── UpdateProductCommand.cs
│   ├── Queries/
│   │   └── GetProductsQuery.cs
│   ├── DTOs/
│   │   └── ProductDto.cs
│   └── Services/
│       └── ApplicationProductService.cs
│
├── Infrastructure/
│   ├── Data/
│   │   ├── AppDbContext.cs
│   │   └── Repositories/
│   │       └── ProductRepository.cs
│   └── Services/
│       └── EmailService.cs
│
└── Presentation/
    └── Controllers/
        └── ProductsController.cs

DDD (Domain-Driven Design)

Domain Entities

public class Order : Entity
{
    public Guid Id { get; private set; }
    public CustomerId CustomerId { get; private set; }
    public OrderStatus Status { get; private set; }
    public List<OrderItem> Items { get; private set; }
    public Money TotalAmount { get; private set; }
    
    private Order() { } // For EF Core
    
    public static Order Create(CustomerId customerId)
    {
        return new Order
        {
            Id = Guid.NewGuid(),
            CustomerId = customerId,
            Status = OrderStatus.Draft,
            Items = new List<OrderItem>(),
            TotalAmount = Money.Zero
        };
    }
    
    public void AddItem(Product product, int quantity)
    {
        // Domain logic
        if (Status != OrderStatus.Draft)
            throw new DomainException("Cannot add items to confirmed order");
            
        Items.Add(OrderItem.Create(product, quantity));
        RecalculateTotal();
    }
}

Value Objects

public record Money(decimal Amount, string Currency)
{
    public static Money Zero => new(0, "USD");
    
    public static Money operator +(Money a, Money b)
    {
        if (a.Currency != b.Currency)
            throw new DomainException("Cannot add different currencies");
        return new Money(a.Amount + b.Amount, a.Currency);
    }
}

public record Address(
    string Street,
    string City,
    string State,
    string ZipCode,
    string Country)
{
    public string FullAddress => $"{Street}, {City}, {State} {ZipCode}, {Country}";
}

Aggregates

public class OrderAggregate
{
    private readonly List<OrderItem> _items = new();
    
    public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();
    
    public void AddItem(Product product, int quantity)
    {
        // Aggregate boundary - maintain invariants
        if (quantity <= 0)
            throw new DomainException("Quantity must be positive");
            
        var existing = _items.FirstOrDefault(i => i.ProductId == product.Id);
        if (existing != null)
            existing.IncreaseQuantity(quantity);
        else
            _items.Add(OrderItem.Create(product, quantity));
    }
}

CQRS (Command Query Responsibility Segregation)

Command Handler

public class CreateProductCommand : IRequest<ProductDto>
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int CategoryId { get; set; }
}

public class CreateProductCommandHandler 
    : IRequestHandler<CreateProductCommand, ProductDto>
{
    private readonly AppDbContext _context;
    
    public CreateProductCommandHandler(AppDbContext context)
    {
        _context = context;
    }
    
    public async Task<ProductDto> Handle(
        CreateProductCommand request, 
        CancellationToken cancellationToken)
    {
        var product = new Product
        {
            Name = request.Name,
            Price = request.Price,
            CategoryId = request.CategoryId
        };
        
        _context.Products.Add(product);
        await _context.SaveChangesAsync(cancellationToken);
        
        return new ProductDto
        {
            Id = product.Id,
            Name = product.Name,
            Price = product.Price
        };
    }
}

Query Handler

public class GetProductsQuery : IRequest<List<ProductDto>>
{
    public int? CategoryId { get; set; }
    public int Page { get; set; } = 1;
    public int PageSize { get; set; } = 10;
}

public class GetProductsQueryHandler 
    : IRequestHandler<GetProductsQuery, List<ProductDto>>
{
    private readonly AppDbContext _context;
    
    public GetProductsQueryHandler(AppDbContext context)
    {
        _context = context;
    }
    
    public async Task<List<ProductDto>> Handle(
        GetProductsQuery request, 
        CancellationToken cancellationToken)
    {
        var query = _context.Products
            .AsNoTracking();
            
        if (request.CategoryId.HasValue)
            query = query.Where(p => p.CategoryId == request.CategoryId);
            
        return await query
            .Select(p => new ProductDto
            {
                Id = p.Id,
                Name = p.Name,
                Price = p.Price
            })
            .Skip((request.Page - 1) * request.PageSize)
            .Take(request.PageSize)
            .ToListAsync(cancellationToken);
    }
}

MediatR Integration

// Program.cs
builder.Services.AddMediatR(cfg => 
    cfg.RegisterServicesFromAssembly(typeof(CreateProductCommand).Assembly));

// Controller
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IMediator _mediator;
    
    public ProductsController(IMediator mediator)
    {
        _mediator = mediator;
    }
    
    [HttpPost]
    public async Task<IActionResult> Create([FromBody] CreateProductCommand command)
    {
        var result = await _mediator.Send(command);
        return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
    }
    
    [HttpGet]
    public async Task<IActionResult> GetAll([FromQuery] GetProductsQuery query)
    {
        var result = await _mediator.Send(query);
        return Ok(result);
    }
}

Separate Read/Write Models

// Write Model (Command)
public class CreateOrderCommand
{
    public List<CreateOrderItemCommand> Items { get; set; }
    public Guid CustomerId { get; set; }
}

// Read Model (Query)
public class OrderSummaryDto
{
    public Guid Id { get; set; }
    public string CustomerName { get; set; }
    public int ItemCount { get; set; }
    public decimal TotalAmount { get; set; }
    public string Status { get; set; }
}

Clean Architecture

Overview

Clean Architecture là một architectural style tập trung vào việc tách code thành các layers có dependencies chỉ hướng vào trong (inward direction). Mục tiêu là tạo ra codebase dễ test, maintain, và independent với frameworks, databases, UI.

Layer Structure

┌─────────────────────────────────────┐
│     Presentation (API, UI)          │  ← Outer Layer
├─────────────────────────────────────┤
│     Application (Use Cases)         │
├─────────────────────────────────────┤
│     Domain (Entities, Business)     │  ← Inner Layer (No dependencies)
├─────────────────────────────────────┤
│     Infrastructure (DB, External)   │
└─────────────────────────────────────┘

1. Domain Layer (Innermost)

  • Entities: Business objects với identity
  • Value Objects: Immutable objects without identity
  • Domain Services: Business logic không thuộc về entities
  • Repository Interfaces: Contracts (không implementations)
// Domain/Entities/Order.cs
public class Order
{
    public Guid Id { get; private set; }
    public List<OrderItem> Items { get; private set; }
    public OrderStatus Status { get; private set; }
    
    public void AddItem(Product product, int quantity)
    {
        // Business logic here
        Items.Add(new OrderItem(product, quantity));
    }
    
    public void Place()
    {
        // Domain rules
        if (Items.Count == 0)
            throw new InvalidOperationException("Cannot place empty order");
        Status = OrderStatus.Placed;
    }
}

// Domain/Repositories/IOrderRepository.cs
public interface IOrderRepository
{
    Task<Order> GetByIdAsync(Guid id);
    Task AddAsync(Order order);
    Task UpdateAsync(Order order);
}

2. Application Layer

  • Use Cases / Services: Orchestrate business logic
  • DTOs: Data transfer objects
  • Interfaces: Ports for external services
// Application/UseCases/PlaceOrderUseCase.cs
public class PlaceOrderUseCase
{
    private readonly IOrderRepository _orderRepository;
    private readonly IInventoryService _inventoryService;
    private readonly INotificationService _notificationService;
    
    public PlaceOrderUseCase(
        IOrderRepository orderRepository,
        IInventoryService inventoryService,
        INotificationService notificationService)
    {
        _orderRepository = orderRepository;
        _inventoryService = inventoryService;
        _notificationService = notificationService;
    }
    
    public async Task ExecuteAsync(PlaceOrderCommand command)
    {
        var order = new Order();
        
        foreach (var item in command.Items)
        {
            var product = await _inventoryService.GetProductAsync(item.ProductId);
            order.AddItem(product, item.Quantity);
        }
        
        order.Place();
        await _orderRepository.AddAsync(order);
        
        await _notificationService.SendAsync(order.CustomerEmail, "Order placed!");
    }
}

3. Infrastructure Layer

  • Repository Implementations: Concrete implementations
  • External Services: APIs, Email, Cache
  • Database: Entity Framework, Dapper
// Infrastructure/Persistence/OrderRepository.cs
public class OrderRepository : IOrderRepository
{
    private readonly ApplicationDbContext _context;
    
    public OrderRepository(ApplicationDbContext context)
    {
        _context = context;
    }
    
    public async Task<Order> GetByIdAsync(Guid id)
    {
        return await _context.Orders
            .Include(o => o.Items)
            .FirstOrDefaultAsync(o => o.Id == id);
    }
    
    public async Task AddAsync(Order order)
    {
        await _context.Orders.AddAsync(order);
        await _context.SaveChangesAsync();
    }
}

4. Presentation Layer

  • API Controllers: HTTP endpoints
  • View Models: UI models
// Presentation/Controllers/OrdersController.cs
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    private readonly PlaceOrderUseCase _placeOrderUseCase;
    
    public OrdersController(PlaceOrderUseCase placeOrderUseCase)
    {
        _placeOrderUseCase = placeOrderUseCase;
    }
    
    [HttpPost]
    public async Task<IActionResult> PlaceOrder([FromBody] PlaceOrderCommand command)
    {
        await _placeOrderUseCase.ExecuteAsync(command);
        return Ok();
    }
}

Key Principles

1. Dependency Rule

  • Dependencies chỉ được đi từ outer layers vào inner layers
  • Inner layers không biết gì về outer layers
  • Domain layer là hoàn toàn independent

2. Separation of Concerns

  • Mỗi layer chỉ quan tâm đến một responsibility
  • Business logic trong Domain layer
  • Orchestration trong Application layer

3. Testability

  • Domain layer có thể test không cần database
  • Use cases có thể test với mocks
// Testing Domain Layer
[Fact]
public void Order_Place_WithNoItems_ThrowsException()
{
    var order = new Order();
    
    Assert.Throws<InvalidOperationException>(() => order.Place());
}

Benefits

BenefitDescription
TestabilityEasy to unit test business logic
MaintainabilityClear structure, easy to navigate
IndependenceNot tied to frameworks or databases
Business FocusDomain logic is central and clear
FlexibilityEasy to change UI or infrastructure

Comparison with Other Patterns

PatternFocusComplexity
Clean ArchitectureBusiness logic independenceHigh
Hexagonal ArchitecturePort/Adapter separationMedium
Onion ArchitectureLayered dependenciesMedium
Layered ArchitectureTraditional N-tierLow

When to Use

  • Large enterprise applications
  • Complex business logic
  • Long-lived projects
  • Teams that need clear structure
  • Projects requiring testability

References

Domain-Driven Design (DDD)

Overview

Domain-Driven Design (DDD) là một approach trong software development tập trung vào việc model hóa business domain. Thay vì tập trung vào database hay UI, DDD đặt business logic và domain models làm trung tâm của ứng dụng.

Core Concepts

1. Ubiquitous Language

Ngôn ngữ chung được sử dụng bởi tất cả team members (developers, domain experts, business users) để mô tả domain.

// Thay vì "Order" hay "SalesOrder"
// Sử dụng một tên nhất quán trong code, documentation, và conversations
public class PurchaseOrder  // Domain language
{
    // Không phải "CustomerId" mà là "BuyerId" nếu business gọi là "buyer"
    public Buyer Buyer { get; }
    public List<LineItem> LineItems { get; }
    public OrderStatus Status { get; }
    
    public void Submit() { }  // Không phải "Place" hay "Create"
    public void Confirm() { } // Không phải "Approve"
}

2. Bounded Context

Mỗi domain model có một boundary rõ ràng nơi nó có nghĩa. Different contexts có thể có different models for the same concept.

┌─────────────────┐     ┌─────────────────┐
│  Order Context  │     │  Shipping       │
│                 │     │  Context        │
│  - Order        │     │  - Shipment     │
│  - Customer     │     │  - Delivery      │
│  - Payment      │     │  - Address      │
└─────────────────┘     └─────────────────┘
         │                      │
         │    Shared            │
         │    Language          │
         ▼                      ▼
    (Giữ riêng biệt, có integration point)

Building Blocks

1. Entities

Objects có identity riêng biệt, không chỉ dựa trên attributes.

public class Order : Entity
{
    public OrderId Id { get; private set; }  // Identity
    public Customer Customer { get; private set; }
    public List<OrderItem> Items { get; private set; }
    public OrderStatus Status { get; private set; }
    public DateTime CreatedAt { get; private set; }
    
    // Identity matters - two orders with same data are still different
    // e.g., Order #1001 vs Order #1002
    
    public void AddItem(Product product, int quantity)
    {
        // Business logic
        if (Status != OrderStatus.Draft)
            throw new InvalidOperationException("Cannot add items to placed order");
            
        Items.Add(new OrderItem(product, quantity));
    }
    
    public void Place()
    {
        if (!Items.Any())
            throw new InvalidOperationException("Cannot place empty order");
            
        Status = OrderStatus.Placed;
        AddDomainEvent(new OrderPlacedEvent(this));
    }
}

2. Value Objects

Objects không có identity, chỉ defined by their attributes.

// Two addresses with same values are considered equal
public class Address : ValueObject
{
    public string Street { get; }
    public string City { get; }
    public string State { get; }
    public string ZipCode { get; }
    
    protected override IEnumerable<object> GetEqualityComponents()
    {
        yield return Street;
        yield return City;
        yield return State;
        yield return ZipCode;
    }
}

// Money - another common value object
public class Money : ValueObject
{
    public decimal Amount { get; }
    public Currency Currency { get; }
    
    public Money Add(Money other)
    {
        if (Currency != other.Currency)
            throw new InvalidOperationException("Cannot add different currencies");
            
        return new Money(Amount + other.Amount, Currency);
    }
}

3. Aggregates

Group of related entities and value objects treated as a single unit.

// OrderAggregate - root entity
public class Order : AggregateRoot
{
    private readonly List<OrderItem> _items = new();
    
    public OrderId Id { get; private set; }
    public CustomerId CustomerId { get; private set; }
    public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();
    
    public void AddItem(Product product, int quantity)
    {
        // All invariants enforced here
        var existingItem = _items.FirstOrDefault(i => i.ProductId == product.Id);
        
        if (existingItem != null)
        {
            existingItem.IncreaseQuantity(quantity);
        }
        else
        {
            _items.Add(new OrderItem(product, quantity));
        }
    }
    
    // Only OrderAggregate can create OrderItem
    // External code cannot bypass invariants
}

public class OrderItem
{
    public OrderItemId Id { get; private set; }
    public ProductId ProductId { get; private set; }
    public int Quantity { get; private set; }
    public Money UnitPrice { get; private set; }
    
    internal OrderItem(Product product, int quantity)  // Internal constructor
    {
        // ...
    }
    
    internal void IncreaseQuantity(int quantity)
    {
        Quantity += quantity;
    }
}

4. Domain Services

Business logic không thuộc về Entity hoặc Value Object.

public class PricingService
{
    public Money CalculateOrderTotal(Order order, Discount discount)
    {
        var subtotal = order.Items
            .Sum(i => i.UnitPrice.Amount * i.Quantity);
            
        var discountAmount = subtotal * discount.Percentage;
        
        return new Money(subtotal - discountAmount, Currency.USD);
    }
}

public class InventoryService
{
    public bool IsAvailable(ProductId productId, int quantity)
    {
        // Cross-aggregate business logic
        var product = _productRepository.GetById(productId);
        var reserved = _reservationRepository.GetReservedQuantity(productId);
        
        return product.StockQuantity - reserved >= quantity;
    }
}

5. Repositories

Abstraction cho việc access domain objects.

public interface IOrderRepository
{
    Order GetById(OrderId id);
    Task<Order> GetByIdAsync(OrderId id);
    void Add(Order order);
    void Update(Order order);
    // Not Delete - domain determines when to "archive"
}

// Infrastructure implementation
public class EfOrderRepository : IOrderRepository
{
    private readonly DbContext _context;
    
    public Order GetById(OrderId id)
    {
        return _context.Orders
            .Include(o => o.Items)
            .FirstOrDefault(o => o.Id == id);
    }
    
    public void Add(Order order)
    {
        _context.Orders.Add(order);
    }
}

6. Domain Events

Events that represent something that happened in the domain.

public abstract class DomainEvent
{
    public Guid Id { get; } = Guid.NewGuid();
    public DateTime OccurredAt { get; } = DateTime.UtcNow;
}

public class OrderPlacedEvent : DomainEvent
{
    public OrderId OrderId { get; }
    public CustomerId CustomerId { get; }
    public Money Total { get; }
    
    public OrderPlacedEvent(Order order)
    {
        OrderId = order.Id;
        CustomerId = order.CustomerId;
        Total = order.Total;
    }
}

// Event dispatching
public class Order : AggregateRoot
{
    public void Place()
    {
        Status = OrderStatus.Placed;
        
        // Add domain event
        AddDomainEvent(new OrderPlacedEvent(this));
    }
}

Strategic Patterns

1. Context Mapping

┌──────────────┐     ┌──────────────┐
│   Context A  │     │  Context B  │
│   (Core)     │────▶│  (Supporting)│
│              │  API│              │
└──────────────┘     └──────────────┘

Types of relationships:
- Partnership: Teams work together
- Customer-Supplier: One serves another
- Conformist: Downstream conforms to upstream
- Anticorruption Layer: Protect from upstream changes

2. Anti-Corruption Layer

// Translating between contexts
public class CustomerAdapter
{
    private readonly ExternalCustomerService _externalService;
    
    public Domain.Customer GetCustomer(string externalId)
    {
        var externalCustomer = _externalService.GetById(externalId);
        
        // Transform to domain model
        return new Domain.Customer
        {
            Id = externalCustomer.Id,
            Name = externalCustomer.FullName,
            Email = new Email(externalCustomer.EmailAddress)
        };
    }
}

Implementation in .NET

// Base classes
public abstract class Entity
{
    public Guid Id { get; protected set; }
    
    public bool Equals(Entity other) => Id == other?.Id;
    public override int GetHashCode() => Id.GetHashCode();
}

public abstract class ValueObject
{
    protected abstract IEnumerable<object> GetEqualityComponents();
    
    public override bool Equals(object obj)
    {
        if (obj is not ValueObject other) return false;
        return GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
    }
    
    public override int GetHashCode() 
        => GetEqualityComponents().Aggregate(0, (h, c) => h ^ c.GetHashCode());
}

public abstract class AggregateRoot : Entity
{
    private readonly List<DomainEvent> _domainEvents = new();
    public IReadOnlyList<DomainEvents> DomainEvents => _domainEvents.AsReadOnly();
    
    protected void AddDomainEvent(DomainEvent event)
    {
        _domainEvents.Add(event);
    }
    
    public void ClearDomainEvents() => _domainEvents.Clear();
}

Benefits of DDD

BenefitDescription
Business FocusCode mirrors business language
Complex DomainsBetter handling of complex business logic
CommunicationImproved team communication
TestabilityDomain logic is easily testable
FlexibilityEasier to adapt to changing requirements

When to Use DDD

  • Complex business domains
  • Large teams needing shared understanding
  • Long-lived applications
  • Domain-driven requirements

When NOT to Use DDD

  • Simple CRUD applications
  • Low business complexity
  • Quick prototypes
  • Small teams with simple needs

References

CQRS (Command Query Responsibility Segregation)

Overview

CQRS là một architectural pattern tách biệt operations đọc (read/queries) và ghi (write/commands) thành hai models riêng biệt. Điều này cho phép tối ưu hóa mỗi side một cách độc lập về performance, scalability, và security.

Basic Concept

Traditional Approach:
┌─────────────────────────────────────┐
│          Single Model               │
│  ┌─────────────┐   ┌─────────────┐  │
│  │   Read      │   │   Write     │  │
│  │   (Queries) │   │   (Commands)│  │
│  └─────────────┘   └─────────────┘  │
└─────────────────────────────────────┘

CQRS Approach:
┌─────────────────────────────────────┐
│     Command Side      │  Query Side │
│  ┌─────────────────┐ │ ┌─────────┐  │
│  │  Domain Model   │ │ │  Read   │  │
│  │  (Write Model) │ │ │  Model  │  │
│  └─────────────────┘ │ └─────────┘  │
└─────────────────────────────────────┘

Command vs Query

AspectCommandQuery
PurposeModify stateRead data
Returnvoid/ResultData
Side EffectsYesNo
idempotentUsually notYes
// Commands - Change state
public class CreateOrderCommand : ICommand
{
    public Guid CustomerId { get; set; }
    public List<OrderItemDto> Items { get; set; }
}

public class UpdateOrderStatusCommand : ICommand
{
    public Guid OrderId { get; set; }
    public OrderStatus NewStatus { get; set; }
}

// Queries - Read data
public class GetOrderByIdQuery : IQuery<OrderDto>
{
    public Guid OrderId { get; set; }
}

public class GetCustomerOrdersQuery : IQuery<List<OrderSummaryDto>>
{
    public Guid CustomerId { get; set; }
}

Implementation

1. Models

// Write Model - Domain-focused
public class Order
{
    public Guid Id { get; private set; }
    public Guid CustomerId { get; private set; }
    public List<OrderItem> Items { get; private set; }
    public OrderStatus Status { get; private set; }
    
    public void AddItem(Product product, int quantity)
    {
        // Domain logic
    }
    
    public void Place()
    {
        // Business rules
        Status = OrderStatus.Placed;
    }
}

// Read Model - UI/Presentation-focused
public class OrderDto
{
    public Guid Id { get; set; }
    public string CustomerName { get; set; }
    public List<OrderItemDto> Items { get; set; }
    public decimal TotalAmount { get; set; }
    public string StatusDisplay { get; set; }
    public string FormattedOrderDate { get; set; }
}

public class OrderSummaryDto  // Lightweight for lists
{
    public Guid Id { get; set; }
    public string CustomerName { get; set; }
    public decimal TotalAmount { get; set; }
    public string Status { get; set; }
}

2. Command Handler

public interface ICommandHandler<TCommand> where TCommand : ICommand
{
    Task<Result> HandleAsync(TCommand command);
}

public class CreateOrderHandler : ICommandHandler<CreateOrderCommand>
{
    private readonly IOrderRepository _orderRepository;
    private readonly IEventBus _eventBus;
    
    public CreateOrderHandler(
        IOrderRepository orderRepository,
        IEventBus eventBus)
    {
        _orderRepository = orderRepository;
        _eventBus = eventBus;
    }
    
    public async Task<Result> HandleAsync(CreateOrderCommand command)
    {
        // Validation
        var validationResult = await _validator.ValidateAsync(command);
        if (!validationResult.IsValid)
            return Result.Failure(validationResult.Errors);
        
        // Create order
        var order = new Order(command.CustomerId);
        
        foreach (var item in command.Items)
        {
            var product = await _productRepository.GetByIdAsync(item.ProductId);
            order.AddItem(product, item.Quantity);
        }
        
        // Save
        await _orderRepository.AddAsync(order);
        
        // Publish event
        await _eventBus.PublishAsync(new OrderCreatedEvent(order));
        
        return Result.Success(order.Id);
    }
}

3. Query Handler

public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
    Task<TResult> HandleAsync(TQuery query);
}

public class GetOrderByIdHandler : IQueryHandler<GetOrderByIdQuery, OrderDto>
{
    private readonly IOrderReadRepository _readRepository;
    
    public GetOrderByIdHandler(IOrderReadRepository readRepository)
    {
        _readRepository = readRepository;
    }
    
    public async Task<OrderDto> HandleAsync(GetOrderByIdQuery query)
    {
        return await _readRepository.GetByIdAsync(query.OrderId);
    }
}

public class GetCustomerOrdersHandler : IQueryHandler<GetCustomerOrdersQuery, List<OrderSummaryDto>>
{
    private readonly IOrderReadRepository _readRepository;
    
    public async Task<List<OrderSummaryDto>> HandleAsync(GetCustomerOrdersQuery query)
    {
        return await _readRepository.GetByCustomerIdAsync(query.CustomerId);
    }
}

4. Mediator Pattern for Decoupling

// In ASP.NET Core
public class OrdersController : ControllerBase
{
    private readonly IMediator _mediator;
    
    public OrdersController(IMediator mediator)
    {
        _mediator = mediator;
    }
    
    [HttpPost]
    public async Task<IActionResult> CreateOrder([FromBody] CreateOrderCommand command)
    {
        var result = await _mediator.Send(command);
        
        if (result.IsSuccess)
            return Created($"/orders/{result.Value}", result.Value);
            
        return BadRequest(result.Error);
    }
    
    [HttpGet("{id}")]
    public async Task<OrderDto> GetOrder(Guid id)
    {
        return await _mediator.Send(new GetOrderByIdQuery { OrderId = id });
    }
}

5. Read Database

// Separate read database (optional - can be same DB with different projection)
public interface IOrderReadRepository
{
    Task<OrderDto> GetByIdAsync(Guid id);
    Task<List<OrderSummaryDto>> GetByCustomerIdAsync(Guid customerId);
    Task<List<OrderSummaryDto>> GetRecentOrdersAsync(int count);
}

public class OrderReadRepository : IOrderReadRepository
{
    private readonly ReadDbContext _context;
    
    public async Task<OrderDto> GetByIdAsync(Guid id)
    {
        return await _context.Orders
            .Where(o => o.Id == id)
            .Select(o => new OrderDto
            {
                Id = o.Id,
                CustomerName = o.Customer.Name,
                Items = o.Items.Select(i => new OrderItemDto
                {
                    ProductName = i.Product.Name,
                    Quantity = i.Quantity,
                    UnitPrice = i.UnitPrice
                }).ToList(),
                TotalAmount = o.TotalAmount,
                StatusDisplay = o.Status.ToString(),
                FormattedOrderDate = o.CreatedAt.ToString("dd/MM/yyyy")
            })
            .FirstOrDefaultAsync();
    }
}

Synchronization (Keeping Read/Write in Sync)

Option 1: Synchronous Updates

public class CreateOrderHandler
{
    public async Task<Result> HandleAsync(CreateOrderCommand command)
    {
        // Write to main database
        var order = new Order(command.CustomerId);
        await _writeRepository.AddAsync(order);
        
        // Immediately update read database
        await _readRepository.InsertAsync(MapToReadModel(order));
        
        return Result.Success(order.Id);
    }
}

Option 2: Event-Based (Async)

// Write to event store
public class CreateOrderHandler
{
    public async Task<Result> HandleAsync(CreateOrderCommand command)
    {
        var order = new Order(command.CustomerId);
        await _orderRepository.AddAsync(order);
        
        // Publish event
        await _eventBus.PublishAsync(new OrderCreatedEvent(order));
    }
}

// Event handler updates read database
public class OrderCreatedEventHandler : IEventHandler<OrderCreatedEvent>
{
    private readonly IOrderReadRepository _readRepository;
    
    public async Task HandleAsync(OrderCreatedEvent evt)
    {
        var readModel = new OrderDto
        {
            Id = evt.Order.Id,
            // ... map fields
        };
        
        await _readRepository.InsertAsync(readModel);
    }
}

Benefits

BenefitDescription
Independent ScalingScale reads and writes separately
PerformanceOptimized query models for reads
FlexibilityDifferent data stores for different purposes
SecurityEasier to secure write operations
ComplexityComplex domain logic isolated in commands

Challenges

ChallengeDescription
ComplexityMore moving parts than simple CRUD
Consistencyeventual consistency between read/write
Learning CurveTeam needs to understand pattern
OverheadMay be overkill for simple apps

When to Use

  • Complex domains với nhiều business rules
  • High read-to-write ratio applications
  • Applications cần different read và write models
  • Systems cần high scalability for reads
  • Event-driven architectures

When NOT to Use

  • Simple CRUD applications
  • Low complexity domains
  • Teams mới vào CQRS
  • Projects cần rapid development

Comparison

AspectCRUDCQRS
ModelSingle modelSeparate models
ComplexityLowMedium-High
ScalabilityLimitedHigh
ConsistencyImmediateEventual
Best forSimple appsComplex domains

References

Event-Driven Architecture

Overview

Event-Driven Architecture (EDA) là một architectural style trong đó các components giao tiếp với nhau bằng cách gửi và nhận events. Thay vì components gọi trực tiếp nhau, chúng tạo và phản hồi events, cho phép loose coupling và async processing.

Core Concepts

┌─────────────────────────────────────────────────────────────────┐
│                     Event-Driven System                         │
│                                                                  │
│   ┌──────────┐      ┌──────────┐      ┌──────────┐            │
│   │Producer A│      │Producer B│      │Producer C│            │
│   └────┬─────┘      └────┬─────┘      └────┬─────┘            │
│        │                 │                 │                  │
│        └─────────────────┼─────────────────┘                  │
│                          ▼                                      │
│                   ┌──────────┐                                  │
│                   │ Event    │                                  │
│                   │ Bus/     │                                  │
│                   │ Broker   │                                  │
│                   └────┬─────┘                                  │
│        ┌────────────────┼────────────────┐                    │
│        ▼                ▼                ▼                    │
│   ┌──────────┐    ┌──────────┐    ┌──────────┐               │
│   │Consumer 1│    │Consumer 2│    │Consumer 3│               │
│   └──────────┘    └──────────┘    └──────────┘               │
└─────────────────────────────────────────────────────────────────┘

Components

  1. Event Producer: Tạo và publish events
  2. Event Consumer: Lắng nghe và xử lý events
  3. Event Channel/Broker: Truyền events từ producers đến consumers
  4. Event Router: Định tuyến events đến đúng consumers

Types of Events

1. Simple Events

Một event đơn lẻ, không có batch hay state change history.

public class OrderPlacedEvent
{
    public Guid OrderId { get; set; }
    public Guid CustomerId { get; set; }
    public decimal TotalAmount { get; set; }
    public DateTime PlacedAt { get; set; }
}

2. Event Carried State Transfer

Event chứa dữ liệu cần thiết, consumer không cần gọi API khác.

public class OrderPlacedEvent
{
    public Guid OrderId { get; set; }
    public CustomerInfo Customer { get; set; }  // Full details
    public List<OrderItemDto> Items { get; set; }  // All items
    public decimal TotalAmount { get; set; }
    public string ShippingAddress { get; set; }
}

3. Notification Events

Chỉ notify, consumer tự fetch thêm data nếu cần.

public class OrderPlacedNotification
{
    public Guid OrderId { get; set; }
    public DateTime Timestamp { get; set; }
    // Consumer calls GET /api/orders/{id} to get full details
}

Implementation in .NET

1. Using In-Memory Events (Simple)

// Event definitions
public interface IEvent
{
    DateTime OccurredAt { get; }
}

public class OrderPlacedEvent : IEvent
{
    public DateTime OccurredAt { get; } = DateTime.UtcNow;
    public Guid OrderId { get; }
    public string CustomerEmail { get; }
    
    public OrderPlacedEvent(Guid orderId, string customerEmail)
    {
        OrderId = orderId;
        CustomerEmail = customerEmail;
    }
}

// Event bus (simple in-memory)
public interface IEventBus
{
    void Publish<T>(T @event) where T : IEvent;
    void Subscribe<T>(Action<T> handler) where T : IEvent;
}

public class InMemoryEventBus : IEventBus
{
    private readonly Dictionary<Type, List<Delegate>> _handlers = new();
    
    public void Publish<T>(T @event) where T : IEvent
    {
        if (_handlers.TryGetValue(typeof(T), out var handlers))
        {
            foreach (var handler in handlers)
            {
                ((Action<T>)handler)(@event);
            }
        }
    }
    
    public void Subscribe<T>(Action<T> handler) where T : IEvent
    {
        if (!_handlers.ContainsKey(typeof(T)))
        {
            _handlers[typeof(T)] = new List<Delegate>();
        }
        _handlers[typeof(T)].Add(handler);
    }
}

2. Using Message Queue (Production)

// Using RabbitMQ
public class RabbitMqEventBus : IEventBus
{
    private readonly IConnection _connection;
    private readonly IModel _channel;
    
    public RabbitMqEventBus(IConnection connection)
    {
        _connection = connection;
        _channel = _connection.CreateModel();
    }
    
    public void Publish<T>(T @event) where T : IEvent
    {
        var exchange = "events";
        var routingKey = typeof(T).Name;
        
        _channel.ExchangeDeclare(exchange, ExchangeType.Fanout);
        
        var message = JsonSerializer.Serialize(@event);
        var body = Encoding.UTF8.GetBytes(message);
        
        var properties = _channel.CreateBasicProperties();
        properties.Persistent = true;
        
        _channel.BasicPublish(exchange, routingKey, properties, body);
    }
}

// Consumer
public class OrderEventConsumer : BackgroundService
{
    private readonly IModel _channel;
    
    public OrderEventConsumer(IModel channel)
    {
        _channel = channel;
    }
    
    protected override Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _channel.ExchangeDeclare("events", ExchangeType.Fanout);
        var queue = _channel.QueueDeclare().QueueName;
        _channel.QueueBind(queue, "events", "");
        
        var consumer = new EventingBasicConsumer(_channel);
        consumer.Received += (model, ea) =>
        {
            var body = ea.Body.ToArray();
            var message = Encoding.UTF8.GetString(body);
            var @event = JsonSerializer.Deserialize<OrderPlacedEvent>(message);
            
            ProcessOrderPlaced(@event);
        };
        
        _channel.BasicConsume(queue, autoAck: true, consumer: consumer);
        
        return Task.CompletedTask;
    }
}

3. Using Azure Service Bus

public class AzureServiceBusEventBus : IEventBus
{
    private readonly ServiceBusClient _client;
    private readonly ServiceBusSender _sender;
    
    public AzureServiceBusEventBus(string connectionString)
    {
        _client = new ServiceBusClient(connectionString);
        _sender = _client.CreateSender("events");
    }
    
    public async Task PublishAsync<T>(T @event) where T : IEvent
    {
        var message = new ServiceBusMessage
        {
            Body = new BinaryData(JsonSerializer.Serialize(@event)),
            ContentType = "application/json"
        };
        
        await _sender.SendMessageAsync(message);
    }
}

// Azure Function with Service Bus trigger
public class OrderFunctions
{
    [FunctionName("ProcessOrderPlaced")]
    public async Task Run(
        [ServiceBusTrigger("events", "OrderPlacedEvent")] 
        ServiceBusMessage message)
    {
        var orderEvent = JsonSerializer.Deserialize<OrderPlacedEvent>(
            message.Body.ToString());
            
        await _orderService.ProcessOrderAsync(orderEvent.OrderId);
    }
}

Event Patterns

1. Pub/Sub Pattern

// Multiple consumers can subscribe to same event
public class NotificationService
{
    public void Handle(OrderPlacedEvent evt)
    {
        // Send email notification
        _emailService.Send(evt.CustomerEmail, "Order placed!");
    }
}

public class InventoryService
{
    public void Handle(OrderPlacedEvent evt)
    {
        // Reserve inventory
        _inventoryService.ReserveItems(evt.OrderId);
    }
}

public class AnalyticsService
{
    public void Handle(OrderPlacedEvent evt)
    {
        // Track analytics
        _analytics.TrackOrder(evt.OrderId, evt.TotalAmount);
    }
}

2. Event Sourcing

// Events are stored as the source of truth
public class OrderService
{
    public async Task PlaceOrder(PlaceOrderCommand command)
    {
        var order = new Order();
        
        // Apply domain events
        foreach (var item in command.Items)
        {
            order.AddItem(item.ProductId, item.Quantity);
        }
        
        // Save to event store
        await _eventStore.AppendAsync(order.DomainEvents);
        
        // Publish events
        foreach (var evt in order.DomainEvents)
        {
            await _eventBus.PublishAsync(evt);
        }
    }
}

3. Choreography (Distributed)

// Services communicate directly via events
// No central orchestrator
public class OrderService
{
    public async Task PlaceOrder(Order order)
    {
        await _eventBus.PublishAsync(new OrderCreatedEvent(order));
    }
}

public class InventoryService
{
    public async Task Handle(OrderCreatedEvent evt)
    {
        // Reserve inventory
        await _inventoryService.ReserveAsync(evt.OrderId);
        
        // Publish next event
        await _eventBus.PublishAsync(new InventoryReservedEvent(evt.OrderId));
    }
}

public class PaymentService
{
    public async Task Handle(InventoryReservedEvent evt)
    {
        // Process payment
        await _paymentService.ProcessAsync(evt.OrderId);
        
        await _eventBus.PublishAsync(new PaymentProcessedEvent(evt.OrderId));
    }
}

4. Orchestration (Centralized)

// Orchestrator coordinates the flow
public class OrderOrchestrator
{
    public async Task PlaceOrder(Order order)
    {
        // Step 1: Validate order
        await _orderService.ValidateAsync(order);
        
        // Step 2: Reserve inventory
        await _inventoryService.ReserveAsync(order.Id);
        
        // Step 3: Process payment
        await _paymentService.ProcessAsync(order.Id);
        
        // Step 4: Confirm order
        await _orderService.ConfirmAsync(order.Id);
    }
}

Error Handling

1. Retry with Backoff

public async Task Handle(OrderPlacedEvent evt)
{
    var retryPolicy = Policy
        .Handle<Exception>()
        .WaitAndRetryAsync(3, retryAttempt => 
            TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
            
    await retryPolicy.ExecuteAsync(async () =>
    {
        await _inventoryService.ReserveAsync(evt.OrderId);
    });
}

2. Dead Letter Queue

public async Task Handle(OrderPlacedEvent evt)
{
    try
    {
        await _inventoryService.ReserveAsync(evt.OrderId);
    }
    catch (Exception ex)
    {
        // Send to dead letter queue
        await _deadLetterQueue.SendAsync(new FailedEvent
        {
            OriginalEvent = evt,
            Error = ex.Message,
            FailedAt = DateTime.UtcNow
        });
    }
}

3. Idempotency

public async Task Handle(OrderPlacedEvent evt)
{
    // Check if already processed
    var isProcessed = await _redis.SetAddAsync(
        $"processed:{evt.OrderId}",
        "OrderPlaced");
        
    if (!isProcessed)
    {
        return; // Already processed
    }
    
    await _inventoryService.ReserveAsync(evt.OrderId);
}

Benefits

BenefitDescription
Loose CouplingComponents don’t know about each other
ScalabilityScale consumers independently
FlexibilityEasy to add new consumers
ResilienceFailed events can be retried
Audit TrailEvents provide natural audit trail
Async ProcessingNon-blocking operations

Challenges

ChallengeDescription
ComplexityHarder to debug và trace
Eventual ConsistencyData may not be immediately consistent
DuplicationSame event processed by multiple consumers
OrderingEvents may arrive out of order
TestingHarder to test distributed systems

Use Cases

Use CaseDescription
MicroservicesService-to-service communication
Real-time ProcessingStream processing
NotificationsPush notifications
AuditAudit logging
WorkflowMulti-step processes
IoTDevice event processing

Best Practices

  1. Idempotency: Design consumers to handle duplicate events
  2. Event Size: Keep events small, reference data by ID
  3. Versioning: Plan for event schema changes
  4. Error Handling: Use retry policies và dead letter queues
  5. Monitoring: Track event processing metrics

References

Hexagonal Architecture

Overview

Hexagonal Architecture (còn gọi là Ports and Adapters) là một architectural pattern tập trung vào việc tách biệt application core khỏi các external concerns như databases, UI, và external services. Mục tiêu là tạo ra application có thể test được và independent với infrastructure.

Core Concept

                    ┌──────────────────┐
                    │   Application    │
                    │      Core        │
                    │  (Domain Logic)  │
                    └────────┬─────────┘
                             │
           ┌─────────────────┼─────────────────┐
           │                 │                 │
           ▼                 ▼                 ▼
    ┌────────────┐   ┌────────────┐   ┌────────────┐
    │   Ports    │   │   Ports    │   │   Ports    │
    │ (Input)    │   │ (Output)   │   │ (Output)   │
    └─────┬──────┘   └─────┬──────┘   └─────┬──────┘
          │                │                │
          ▼                ▼                ▼
    ┌────────────┐   ┌────────────┐   ┌────────────┐
    │  Adapters  │   │  Adapters  │   │  Adapters  │
    │ (REST API) │   │ (Database) │   │ (External) │
    └────────────┘   └────────────┘   └────────────┘

Architecture Components

1. Domain (Core)

  • Business entities và rules
  • Không phụ thuộc vào bất kỳ external framework
// Domain/Entities/Product.cs
public class Product
{
    public Guid Id { get; private set; }
    public string Name { get; private set; }
    public Money Price { get; private set; }
    
    public void ApplyDiscount(Percentage discount)
    {
        Price = Price * (1 - discount.Value);
    }
}

// Domain/ValueObjects/Money.cs
public class Money
{
    public decimal Amount { get; }
    public string Currency { get; }
    
    public Money(decimal amount, string currency = "USD")
    {
        Amount = amount;
        Currency = currency;
    }
    
    public static Money operator +(Money a, Money b) 
        => new Money(a.Amount + b.Amount, a.Currency);
    
    public static Money operator *(Money a, decimal factor)
        => new Money(a.Amount * factor, a.Currency);
}

2. Ports (Interfaces)

Ports là contracts định nghĩa cách application giao tiếp với outside world.

// Ports/Input/IOrderService.cs (Primary Port)
public interface IOrderService
{
    Task<OrderResult> CreateOrderAsync(CreateOrderCommand command);
    Task<OrderResult> GetOrderAsync(Guid orderId);
}

// Ports/Output/IOrderRepository.cs (Secondary Port)
public interface IOrderRepository
{
    Task<Order> FindByIdAsync(Guid id);
    Task SaveAsync(Order order);
}

// Ports/Output/INotificationService.cs (Secondary Port)
public interface INotificationService
{
    Task SendAsync(string recipient, string message);
}

3. Adapters (Implementations)

Adapters là implementations của ports, kết nối application với external systems.

// Adapters/Persistence/OrderRepository.cs
public class OrderRepository : IOrderRepository
{
    private readonly DbContext _context;
    
    public OrderRepository(DbContext context)
    {
        _context = context;
    }
    
    public async Task<Order> FindByIdAsync(Guid id)
    {
        return await _context.Orders.FindAsync(id);
    }
    
    public async Task SaveAsync(Order order)
    {
        _context.Orders.Add(order);
        await _context.SaveChangesAsync();
    }
}

// Adapters/Primary/OrderController.cs
public class OrderController : IOrderService
{
    private readonly IOrderService _orderService;
    
    public async Task<OrderResult> CreateOrderAsync(CreateOrderCommand command)
    {
        return await _orderService.CreateOrderAsync(command);
    }
}

4. Application Services

// Application/OrderApplicationService.cs
public class OrderApplicationService : IOrderService
{
    private readonly IOrderRepository _orderRepository;
    private readonly INotificationService _notificationService;
    
    public OrderApplicationService(
        IOrderRepository orderRepository,
        INotificationService notificationService)
    {
        _orderRepository = orderRepository;
        _notificationService = notificationService;
    }
    
    public async Task<OrderResult> CreateOrderAsync(CreateOrderCommand command)
    {
        var order = new Order();
        
        foreach (var item in command.Items)
        {
            order.AddItem(item.ProductId, item.Quantity);
        }
        
        await _orderRepository.SaveAsync(order);
        
        await _notificationService.SendAsync(
            command.CustomerEmail,
            $"Order {order.Id} created successfully");
        
        return new OrderResult(order.Id, order.Total);
    }
}

Dependency Flow

                    ┌─────────────────┐
                    │  Controller/    │
                    │   Consumer      │
                    └────────┬────────┘
                             │ depends on
                             ▼
                    ┌─────────────────┐
                    │     Port        │  ← Interface (Application owns)
                    │   (Input)       │
                    └────────┬────────┘
                             │ implements
                             ▼
                    ┌─────────────────┐
                    │  Application     │
                    │    Service       │
                    └────────┬────────┘
                             │ depends on
                             ▼
                    ┌─────────────────┐
                    │     Port        │  ← Interface (Application owns)
                    │   (Output)      │
                    └────────┬────────┘
                             │ implements
                             ▼
                    ┌─────────────────┐
                    │     Adapter     │  ← Implementation (Infrastructure)
                    │  (Repository)   │
                    └─────────────────┘

Benefits

BenefitDescription
TestabilityEasy to mock adapters for testing
FlexibilitySwap adapters without changing core
MaintainabilityClear separation of concerns
Framework IndependenceCore is not tied to any framework
Team ScalabilityDifferent teams can work on different adapters

Use Cases

  • Complex enterprise applications
  • Applications requiring multiple external integrations
  • Systems that need frequent infrastructure changes
  • Applications with complex testing requirements

Comparison with Clean Architecture

AspectHexagonalClean Architecture
FocusPorts & AdaptersLayer dependencies
StructureHexagonal layersCircular layers
DomainPure domainDomain + Application
ComplexityMediumHigh

References

Onion Architecture

Overview

Onion Architecture là một architectural pattern tập trung vào việc tạo layered architecture với dependencies chỉ hướng vào trong. Nó tương tự Clean Architecture nhưng có cách tổ chức khác, nhấn mạnh vào việc tách biệt core domain khỏi infrastructure.

Layer Structure

┌─────────────────────────────────────────────┐
│                Presentation                │  ← Outer Layer
│          (Controllers, APIs, UI)            │
├─────────────────────────────────────────────┤
│              Application                   │
│         (Use Cases, Services)              │
├─────────────────────────────────────────────┤
│                 Domain                      │  ← Core (innermost)
│        (Entities, Value Objects)            │
├─────────────────────────────────────────────┤
│              Infrastructure                 │  ← Outer Layer
│    (Repositories, External Services)       │
└─────────────────────────────────────────────┘

Core Principles

1. Dependency Inward

  • Tất cả dependencies chỉ đi từ outer layers vào inner layers
  • Inner layers không biết gì về outer layers
  • Domain layer là hoàn toàn standalone

2. Coupling

  • Loose coupling giữa các layers
  • Sử dụng interfaces để abstract dependencies

3. Testability

  • Core domain có thể test không cần infrastructure
  • Application services dễ dàng mock dependencies

Layer Details

1. Domain Layer (Core)

// Domain/Entities/Customer.cs
public class Customer
{
    public Guid Id { get; private set; }
    public string Email { get; private set; }
    public string Name { get; private set; }
    
    private Customer() { } // For ORM
    
    public static Customer Create(string email, string name)
    {
        if (string.IsNullOrWhiteSpace(email))
            throw new ArgumentException("Email is required");
            
        return new Customer
        {
            Id = Guid.NewGuid(),
            Email = email,
            Name = name
        };
    }
    
    public void UpdateName(string name)
    {
        if (string.IsNullOrWhiteSpace(name))
            throw new ArgumentException("Name is required");
        Name = name;
    }
}

// Domain/ValueObjects/Email.cs
public class Email
{
    public string Value { get; }
    
    public Email(string value)
    {
        if (!IsValid(value))
            throw new InvalidEmailException(value);
        Value = value;
    }
    
    private bool IsValid(string email) 
        => email.Contains("@") && email.Contains(".");
}

// Domain/Services/OrderDomainService.cs
public class OrderDomainService
{
    public void ApplyDiscount(Order order, Discount discount)
    {
        if (order.Status != OrderStatus.Draft)
            throw new InvalidOperationException("Cannot apply discount to placed order");
            
        order.ApplyDiscount(discount);
    }
}

2. Application Layer

// Application/Interfaces/ICustomerRepository.cs
public interface ICustomerRepository
{
    Task<Customer> GetByIdAsync(Guid id);
    Task AddAsync(Customer customer);
    Task UpdateAsync(Customer customer);
}

// Application/Services/CustomerService.cs
public class CustomerService
{
    private readonly ICustomerRepository _customerRepository;
    
    public CustomerService(ICustomerRepository customerRepository)
    {
        _customerRepository = customerRepository;
    }
    
    public async Task<CustomerDto> CreateCustomerAsync(CreateCustomerCommand command)
    {
        var customer = Customer.Create(command.Email, command.Name);
        
        await _customerRepository.AddAsync(customer);
        
        return new CustomerDto
        {
            Id = customer.Id,
            Email = customer.Email,
            Name = customer.Name
        };
    }
}

// Application/DTOs/CustomerDto.cs
public class CustomerDto
{
    public Guid Id { get; set; }
    public string Email { get; set; }
    public string Name { get; set; }
}

3. Infrastructure Layer

// Infrastructure/Persistence/EfCustomerRepository.cs
public class EfCustomerRepository : ICustomerRepository
{
    private readonly DbContext _context;
    
    public EfCustomerRepository(DbContext context)
    {
        _context = context;
    }
    
    public async Task<Customer> GetByIdAsync(Guid id)
    {
        return await _context.Customers.FindAsync(id);
    }
    
    public async Task AddAsync(Customer customer)
    {
        await _context.Customers.AddAsync(customer);
        await _context.SaveChangesAsync();
    }
    
    public async Task UpdateAsync(Customer customer)
    {
        _context.Customers.Update(customer);
        await _context.SaveChangesAsync();
    }
}

// Infrastructure/Mapping/CustomerMapping.cs
public class CustomerMapping : IEntityTypeConfiguration<Customer>
{
    public void Configure(EntityTypeBuilder<Customer> builder)
    {
        builder.ToTable("Customers");
        builder.HasKey(c => c.Id);
        builder.Property(c => c.Email).IsRequired().HasMaxLength(256);
        builder.Property(c => c.Name).IsRequired().HasMaxLength(128);
    }
}

4. Presentation Layer

// Presentation/Controllers/CustomersController.cs
[ApiController]
[Route("api/[controller]")]
public class CustomersController : ControllerBase
{
    private readonly CustomerService _customerService;
    
    public CustomersController(CustomerService customerService)
    {
        _customerService = customerService;
    }
    
    [HttpPost]
    public async Task<ActionResult<CustomerDto>> Create(
        [FromBody] CreateCustomerCommand command)
    {
        var result = await _customerService.CreateCustomerAsync(command);
        return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
    }
    
    [HttpGet("{id}")]
    public async Task<ActionResult<CustomerDto>> GetById(Guid id)
    {
        var customer = await _customerService.GetCustomerAsync(id);
        if (customer == null)
            return NotFound();
        return Ok(customer);
    }
}

Dependency Injection Setup

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Register Infrastructure
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));

// Register Application Services
builder.Services.AddScoped<ICustomerRepository, EfCustomerRepository>();
builder.Services.AddScoped<CustomerService>();

// Register Controllers
builder.Services.AddControllers();

var app = builder.Build();
app.Run();

Benefits

BenefitDescription
Strong Domain FocusBusiness logic is central
TestabilityEasy to test each layer
FlexibilityEasy to change infrastructure
MaintainabilityClear structure
Framework AgnosticDomain not tied to frameworks

Comparison with Other Patterns

AspectOnionCleanHexagonal
Layers4 layers4+ layersPorts/Adapters
FocusInward dependenciesLayer separationPort/Adapter
DomainEntities, ServicesDomain + Use CasesDomain only
InfrastructureExplicit layerExternalAdapters

When to Use

  • Medium to large applications
  • Domain-driven design projects
  • Teams familiar with layered architecture
  • Projects needing clear boundaries

Key Differences from Clean Architecture

  1. Simplified: Fewer explicit layers
  2. Domain-Centric: Stronger focus on domain
  3. Flexible: More freedom in implementation
  4. Integration: Can integrate with various patterns

References

Phương pháp Phát triển

Overview

Các phương pháp phát triển phần mềm (development methodologies) là các frameworks và practices giúp teams tổ chức và thực hiện công việc một cách hiệu quả. Mỗi method có ưu điểm và phù hợp với các tình huống khác nhau.

Main Categories

1. Traditional/Plan-Based Methods

Waterfall

Requirements → Design → Implementation → Verification → Maintenance
     ↓              ↓            ↓                ↓              ↓
   1990s       1990s-2000s   2000s           2000s         present
  • Sequential, document-driven
  • Each phase complete before next begins
  • Good for: Fixed requirements, stable projects
// Waterfall in practice - comprehensive upfront design
public class ProjectPlan
{
    // Detailed specification before coding
    public RequirementsSpec Requirements { get; set; }
    public ArchitectureSpec Architecture { get; set; }
    public TestPlan TestPlan { get; set; }
    public DeploymentPlan DeploymentPlan { get; set; }
}

V-Model

Requirements ←────────────── Acceptance Testing
    ↓                              ↓
System Design ←─────────────── Integration Testing
    ↓                              ↓
Detailed Design ←─────────────── Unit Testing
    ↓
Coding
  • Extension of Waterfall
  • Testing integrated into each phase
  • Good for: Safety-critical systems

2. Agile Methods

Scrum

Sprint Planning → Daily Scrum → Sprint Review → Sprint Retrospective
       ↓              ↓              ↓                ↓
     2-4 weeks     15 min         1-4 hours       1-3 hours
// Scrum artifacts
public class ProductBacklog
{
    public List<ProductBacklogItem> Items { get; set; }
    public void Prioritize() { } // Ranks by business value
}

public class Sprint
{
    public List<SprintBacklogItem> Backlog { get; set; }
    public SprintGoal Goal { get; set; }
    public Timebox Duration { get; set; } // Usually 2-4 weeks
}

public class DailyScrum
{
    // 3 questions:
    // 1. What did I do yesterday?
    // 2. What will I do today?
    // 3. Are there any blockers?
}

Kanban

To Do → In Progress → Review → Done
  ↓        ↓          ↓        ↓
 limits   limits     limits   limits
// Kanban board in code
public class KanbanBoard
{
    public Column ToDo { get; set; }
    public Column InProgress { get; set; } // WIP limit: 3
    public Column Review { get; set; }     // WIP limit: 2
    public Column Done { get; set; }
    
    public void MoveToInProgress(WorkItem item)
    {
        if (InProgress.Count >= InProgress.WipLimit)
            throw new Exception("WIP limit exceeded");
            
        InProgress.Add(item);
    }
}

Extreme Programming (XP)

Core Practices:
- Pair Programming
- Test-Driven Development
- Continuous Integration
- Refactoring
- Simple Design
- Customer Collaboration
// XP practices
public class XPPractice
{
    // Pair Programming: Two developers at one workstation
    // Continuous Integration: Every commit triggers build + tests
    // TDD: Red → Green → Refactor
}

3. Lean Methods

Principles

  1. Eliminate waste
  2. Amplify learning
  3. Decide as late as possible
  4. Deliver as fast as possible
  5. Build quality in
  6. See the whole
public class LeanPractice
{
    // Waste elimination
    // - Partially done work
    // - Extra features
    // - Waiting
    // - Handoffs
}

4. Modern Approaches

DevOps

Plan → Code → Build → Test → Deploy → Operate → Monitor
    ↑__________________________________________|
           Continuous feedback loop
# DevOps pipeline example
name: CI/CD Pipeline
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Run tests
        run: dotnet test
      - name: Build
        run: dotnet publish
      - name: Deploy
        run: deploy.sh

Continuous Delivery/Deployment

// CI/CD Pipeline stages
public class PipelineStage
{
    public const string BUILD = "Build";
    public const string TEST = "Test";
    public const string STAGING = "Deploy to Staging";
    public const string PRODUCTION = "Deploy to Production";
}

Comparison

MethodFlexibilityDocumentationSpeedTeam Size
WaterfallLowHighLowAny
ScrumHighLowHigh5-9
KanbanHighMediumHighAny
XPHighMediumHighSmall
DevOpsHighLowVery HighAny

Choosing Right Method

Project TypeRecommended Method
Fixed requirements, safety-criticalWaterfall, V-Model
Complex, changing requirementsScrum, Kanban
Small team, fast iterationXP, Scrum
Operations-focusedDevOps
Startup, MVPAgile, Lean

Hybrid Approaches

// Many teams use hybrid approaches
public class HybridMethod
{
    // Scrumban - Scrum + Kanban
    // Water-Scrum-Fall - Waterfall for planning, Scrum for execution
    // SAFe - Scaled Agile Framework for large projects
}

Key Practices

1. Iteration

public class Iteration
{
    // Short, time-boxed cycles
    // Each iteration produces working software
    // Feedback drives next iteration
}

2. Incremental Delivery

public class IncrementalDelivery
{
    // Deliver small pieces frequently
    // Each increment adds functionality
    // Early value to customers
}

3. Empirical Process

public class EmpiricalProcess
{
    // Transparency: Visible progress
    // Inspection: Frequent checkpoints
    // Adaptation: Change based on feedback
}

References

Test-Driven Development (TDD)

Overview

Test-Driven Development (TDD) là một development methodology trong đó tests được viết trước khi viết code. Chu trình cơ bản là: Red - Green - Refactor.

┌─────────────────────────────────────────────────────────────┐
│                    TDD Cycle                                │
│                                                              │
│   ┌──────────┐   ┌──────────┐   ┌─────────────┐             │
│   │   Red    │──▶│  Green   │──▶│  Refactor   │             │
│   │(Fail)    │   │(Pass)    │   │(Improve)    │             │
│   └──────────┘   └──────────┘   └──────┬──────┘             │
│        │              │                  │                   │
│        │              │                  │                   │
│        ▼              ▼                  ▼                   │
│   Write failing   Write minimal     Improve code            │
│   test            code to pass      while keeping           │
│   first           test              tests passing            │
└─────────────────────────────────────────────────────────────┘

The Three Laws of TDD

  1. First Law: Không viết production code cho đến khi có một failing unit test
  2. Second Law: Không viết thêm test gì ngoài cái test đang fail để make it pass
  3. Third Law: Không viết thêm production code ngoài cái để make test pass

Implementation in C#

1. Basic Example - Calculator

// Step 1: Write failing test (Red)
// CalculatorTests.cs
public class CalculatorTests
{
    [Fact]
    public void Add_TwoNumbers_ReturnsSum()
    {
        // Arrange
        var calculator = new Calculator();
        
        // Act
        var result = calculator.Add(2, 3);
        
        // Assert
        Assert.Equal(5, result);
    }
}

// Step 2: Write minimal code (Green)
// Calculator.cs
public class Calculator
{
    public int Add(int a, int b)
    {
        return 5; // Minimal implementation to pass
    }
}

// Step 3: Refactor - implement properly
public class Calculator
{
    public int Add(int a, int b)
    {
        return a + b;
    }
}

2. Bank Account Example

// Domain/BankAccount.cs
public class BankAccount
{
    public decimal Balance { get; private set; }
    
    public void Deposit(decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentException("Amount must be positive", nameof(amount));
            
        Balance += amount;
    }
    
    public void Withdraw(decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentException("Amount must be positive", nameof(amount));
            
        if (amount > Balance)
            throw new InvalidOperationException("Insufficient balance");
            
        Balance -= amount;
    }
}

// Tests/BankAccountTests.cs
public class BankAccountTests
{
    [Theory]
    [InlineData(100, 50, 50)]
    [InlineData(100, 10, 90)]
    [InlineData(0, 0, 0)]
    public void Withdraw_ValidAmount_DecreasesBalance(
        decimal initialBalance, 
        decimal withdrawAmount, 
        decimal expectedBalance)
    {
        // Arrange
        var account = new BankAccount();
        account.Deposit(initialBalance);
        
        // Act
        account.Withdraw(withdrawAmount);
        
        // Assert
        Assert.Equal(expectedBalance, account.Balance);
    }
    
    [Fact]
    public void Withdraw_MoreThanBalance_ThrowsException()
    {
        var account = new BankAccount();
        account.Deposit(100);
        
        Assert.Throws<InvalidOperationException>(() => 
            account.Withdraw(200));
    }
    
    [Theory]
    [InlineData(0)]
    [InlineData(-10)]
    public void Deposit_NonPositiveAmount_ThrowsException(decimal amount)
    {
        var account = new BankAccount();
        
        Assert.Throws<ArgumentException>(() => account.Deposit(amount));
    }
}

3. String Calculator (Kent Beck Style)

// StringCalculator.cs
public class StringCalculator
{
    public int Add(string numbers)
    {
        if (string.IsNullOrEmpty(numbers))
            return 0;
            
        var parts = numbers.Split(',');
        return parts.Sum(int.Parse);
    }
}

// StringCalculatorTests.cs
public class StringCalculatorTests
{
    [Fact]
    public void Add_EmptyString_ReturnsZero()
    {
        var calculator = new StringCalculator();
        
        var result = calculator.Add("");
        
        Assert.Equal(0, result);
    }
    
    [Fact]
    public void Add_SingleNumber_ReturnsThatNumber()
    {
        var result = new StringCalculator().Add("5");
        
        Assert.Equal(5, result);
    }
    
    [Fact]
    public void Add_TwoNumbers_ReturnsSum()
    {
        var result = new StringCalculator().Add("1,2");
        
        Assert.Equal(3, result);
    }
    
    [Fact]
    public void Add_UnknownAmountOfNumbers_ReturnsSum()
    {
        var result = new StringCalculator().Add("1,2,3,4,5");
        
        Assert.Equal(15, result);
    }
}

4. Testing with Dependencies

// Service using interface for dependency
public interface IOrderRepository
{
    Task<Order> GetByIdAsync(Guid id);
}

public class OrderService
{
    private readonly IOrderRepository _repository;
    
    public OrderService(IOrderRepository repository)
    {
        _repository = repository;
    }
    
    public async Task<bool> ValidateOrderAsync(Guid orderId)
    {
        var order = await _repository.GetByIdAsync(orderId);
        
        if (order == null)
            return false;
            
        return order.Status == OrderStatus.Active;
    }
}

// Testing with mocking
public class OrderServiceTests
{
    [Fact]
    public async Task ValidateOrderAsync_ActiveOrder_ReturnsTrue()
    {
        // Arrange
        var mockRepo = new Mock<IOrderRepository>();
        var order = new Order { Id = Guid.NewGuid(), Status = OrderStatus.Active };
        
        mockRepo.Setup(r => r.GetByIdAsync(order.Id))
            .ReturnsAsync(order);
            
        var service = new OrderService(mockRepo.Object);
        
        // Act
        var result = await service.ValidateOrderAsync(order.Id);
        
        // Assert
        Assert.True(result);
    }
    
    [Fact]
    public async Task ValidateOrderAsync_OrderNotFound_ReturnsFalse()
    {
        var mockRepo = new Mock<IOrderRepository>();
        
        mockRepo.Setup(r => r.GetByIdAsync(It.IsAny<Guid>()))
            .ReturnsAsync((Order)null);
            
        var service = new OrderService(mockRepo.Object);
        
        var result = await service.ValidateOrderAsync(Guid.NewGuid());
        
        Assert.False(result);
    }
}

5. Testing Edge Cases

public class OrderTests
{
    [Fact]
    public void PlaceOrder_EmptyItems_ThrowsException()
    {
        var order = new Order();
        
        Assert.Throws<InvalidOperationException>(() => order.Place());
    }
    
    [Fact]
    public void PlaceOrder_AlreadyPlaced_ThrowsException()
    {
        var order = new Order();
        order.AddItem(CreateSampleProduct(), 1);
        order.Place();
        
        Assert.Throws<InvalidOperationException>(() => order.Place());
    }
    
    [Fact]
    public void AddItem_NegativeQuantity_ThrowsException()
    {
        var order = new Order();
        
        Assert.Throws<ArgumentException>(() => 
            order.AddItem(CreateSampleProduct(), -1));
    }
    
    private Product CreateSampleProduct()
    {
        return new Product("Test Product", 10.00m);
    }
}

AAA Pattern

[Fact]
public void Example()
{
    // Arrange - Setup objects, prepare data
    var calculator = new Calculator();
    var expected = 10;
    
    // Act - Execute the functionality
    var result = calculator.Add(6, 4);
    
    // Assert - Verify the result
    Assert.Equal(expected, result);
}

Naming Conventions

// MethodName_Scenario_ExpectedBehavior
[Fact]
public void Withdraw_InsufficientFunds_ThrowsException() { }

[Fact]
public void Add_NegativeNumbers_ThrowsArgumentException() { }

[Fact]
public void ProcessOrder_ValidOrder_ReturnsSuccess() { }

Test Organization

public class OrderServiceTests
{
    // Order Creation Tests
    [Fact] public void CreateOrder_ValidData_ReturnsOrder() { }
    [Fact] public void CreateOrder_NullData_ThrowsException() { }
    
    // Order Modification Tests
    [Fact] public void AddItem_ValidItem_AddsToOrder() { }
    [Fact] public void RemoveItem_ExistingItem_RemovesFromOrder() { }
    
    // Order Processing Tests
    [Fact] public void Place_PlacedOrder_ChangesStatus() { }
    [Fact] public void Cancel_CancellableOrder_ChangesStatus() { }
}

Benefits

BenefitDescription
Better DesignTests force good architecture
Fewer BugsCatch issues early
Living DocumentationTests document the code
ConfidenceRefactor without fear
Faster DebuggingKnow exactly what’s broken

Challenges

ChallengeDescription
Learning CurveTakes time to get comfortable
Over-testingDon’t test trivial code
Test MaintenanceTests need to evolve with code
Slow StartSeems slower at first

Best Practices

  1. Test one thing: Each test should verify one behavior
  2. Use descriptive names: Test names explain behavior
  3. Follow AAA: Arrange, Act, Assert
  4. Keep tests independent: No test should depend on another
  5. Test edge cases: Include boundary conditions
  6. Refactor tests too: Keep test code clean

Test Pyramid

        ┌───────────┐
        │   E2E    │  ← Few tests, slow
        │  Tests   │
      ┌─┴───────────┴─┐
      │  Integration  │  ← More tests
      │    Tests     │
    ┌─┴───────────────┴─┐
    │    Unit Tests    │  ← Many tests, fast
    └───────────────────┘

Tools

  • xUnit: Modern testing framework
  • NUnit: Classic .NET testing
  • Moq: Mocking framework
  • FluentAssertions: Better assertions

References

Behavior-Driven Development (BDD)

Overview

Behavior-Driven Development (BDD) là một agile methodology kết hợp TDD với principles của Domain-Driven Design. BDD tập trung vào behavior của hệ thống từ góc nhìn của stakeholders, sử dụng ngôn ngữ tự nhiên (Gherkin) để mô tả requirements.

Core Principles

1. Discovery

Làm việc với stakeholders để discover behaviors cần thiết.

2. Formulation

Chuyển behaviors thành executable specifications.

3. Automation

Automate specifications để drive development.

Gherkin Syntax

Feature: User Login
  As a registered user
  I want to log in to the system
  So that I can access my personalized content

  Scenario: Successful login
    Given I am on the login page
    And I have a valid account
    When I enter correct credentials
    Then I should be redirected to the dashboard
    And I should see a welcome message

  Scenario: Invalid credentials
    Given I am on the login page
    When I enter incorrect credentials
    Then I should see an error message
    And I should remain on the login page

  Scenario: Locked account after 3 failed attempts
    Given my account is locked
    When I try to log in
    Then I should see an account locked message
    And I should be redirected to the support page

Implementation with SpecFlow

1. Project Setup

<!-- .csproj -->
<PackageReference Include="SpecFlow" Version="3.9.0" />
<PackageReference Include="SpecFlow.xUnit" Version="3.9.0" />
<PackageReference Include="FluentAssertions" Version="6.0.0" />

2. Step Definitions

// Steps/LoginSteps.cs
public class LoginSteps
{
    private readonly LoginPage _loginPage;
    private readonly DashboardPage _dashboardPage;
    private string _errorMessage;
    
    public LoginSteps(LoginPage loginPage, DashboardPage dashboardPage)
    {
        _loginPage = loginPage;
        _dashboardPage = dashboardPage;
    }
    
    [Given(@"I am on the login page")]
    public void GivenIAmOnTheLoginPage()
    {
        _loginPage.NavigateTo();
    }
    
    [Given(@"I have a valid account")]
    public void GivenIHaveAValidAccount()
    {
        // Setup test data or mock
    }
    
    [When(@"I enter correct credentials")]
    public void WhenIEnterCorrectCredentials()
    {
        _loginPage.EnterUsername("testuser");
        _loginPage.EnterPassword("correctpassword");
        _loginPage.ClickLogin();
    }
    
    [Then(@"I should be redirected to the dashboard")]
    public void ThenIShouldBeRedirectedToTheDashboard()
    {
        _dashboardPage.IsDisplayed().Should().BeTrue();
    }
    
    [Then(@"I should see a welcome message")]
    public void ThenIShouldSeeAWelcomeMessage()
    {
        _dashboardPage.GetWelcomeMessage()
            .Should().Contain("Welcome");
    }
}

3. Page Objects

// Pages/LoginPage.cs
public class LoginPage
{
    private readonly IWebDriver _driver;
    
    public LoginPage(IWebDriver driver)
    {
        _driver = driver;
    }
    
    private IWebElement UsernameField => _driver.FindElement(By.Id("username"));
    private IWebElement PasswordField => _driver.FindElement(By.Id("password"));
    private IWebElement LoginButton => _driver.FindElement(By.CssSelector("button[type='submit']"));
    private IWebElement ErrorMessage => _driver.FindElement(By.CssSelector(".error-message"));
    
    public void NavigateTo() => _driver.Navigate().GoToUrl("https://app.example.com/login");
    
    public void EnterUsername(string username) => UsernameField.SendKeys(username);
    public void EnterPassword(string password) => PasswordField.SendKeys(password);
    public void ClickLogin() => LoginButton.Click();
    
    public string GetErrorMessage() => ErrorMessage.Text;
}

4. Hooks and Context

// Hooks/SetupHooks.cs
[Binding]
public class SetupHooks
{
    private readonly ScenarioContext _scenarioContext;
    
    public SetupHooks(ScenarioContext scenarioContext)
    {
        _scenarioContext = scenarioContext;
    }
    
    [BeforeScenario]
    public void BeforeScenario()
    {
        // Setup test database
        TestDatabase.Reset();
        
        // Initialize web driver
        var options = new ChromeOptions();
        options.AddArgument("--headless");
        
        var driver = new ChromeDriver(options);
        _scenarioContext["Driver"] = driver;
    }
    
    [AfterScenario]
    public void AfterScenario()
    {
        // Cleanup
        var driver = _scenarioContext.Get<IWebDriver>("Driver");
        driver?.Quit();
    }
    
    [AfterStep]
    public void AfterStep()
    {
        // Take screenshot on failure
        if (_scenarioContext.TestError != null)
        {
            var driver = _scenarioContext.Get<IWebDriver>("Driver");
            ((ITakesScreenshot)driver).GetScreenshot()
                .SaveAsFile($"screenshots/{Guid.NewGuid()}.png");
        }
    }
}

5. Custom Transform

// Transform steps
[Given(@"I have a valid account")]
public void GivenIHaveAValidAccount()
{
    var user = new User
    {
        Username = "testuser",
        Password = HashPassword("correctpassword"),
        Status = UserStatus.Active
    };
    
    UserRepository.Add(user);
    _scenarioContext.Set(user);
}

// Table transformation
[Given(@"I have the following products in my cart:")]
public void GivenIHaveTheFollowingProductsInMyCart(Table table)
{
    var products = table.CreateSet<CartProduct>();
    foreach (var product in products)
    {
        ShoppingCart.Add(product);
    }
}

BDD in API Testing

// API Testing with BDD
[Binding]
public class ApiSteps
{
    private readonly HttpClient _httpClient;
    private ApiResponse _lastResponse;
    
    [Given(@"I am an authenticated user")]
    public void GivenIAmAnAuthenticatedUser()
    {
        _httpClient.DefaultRequestHeaders.Authorization = 
            new AuthenticationHeaderValue("Bearer", "test-token");
    }
    
    [When(@"I send a GET request to (.*)")]
    public async Task WhenISendAGETRequestTo(string endpoint)
    {
        _lastResponse = await _httpClient.GetAsync(endpoint);
    }
    
    [Then(@"the response status should be (.*)")]
    public void ThenTheResponseStatusShouldBe(HttpStatusCode statusCode)
    {
        _lastResponse.StatusCode.Should().Be(statusCode);
    }
    
    [Then(@"the response should contain:")]
    public void ThenTheResponseShouldContain(Table table)
    {
        var content = JsonSerializer.Deserialize<Dictionary<string, object>>(
            _lastResponse.Content);
            
        foreach (var row in table.Rows)
        {
            var key = row["Field"];
            var expectedValue = row["Value"];
            
            content.Should().ContainKey(key);
            content[key].ToString().Should().Be(expectedValue);
        }
    }
}

Feature File Organization

# Features/Orders/OrderPlacement.feature
Feature: Order Placement
  In order to buy products
  As a customer
  I want to place orders

  Background:
    Given the store has the following products:
      | Product    | Price | Stock |
      | Laptop     | 1000  | 10    |
      | Mouse      | 20    | 50    |
      | Keyboard   | 80    | 30    |
      
  @happy_path
  Scenario: Place an order with available items
    Given my cart contains:
      | Product | Quantity |
      | Laptop  | 1        |
    When I proceed to checkout
    And I complete the payment
    Then my order should be confirmed
    And the inventory should be reduced by 1 for "Laptop"

  @edge_case
  Scenario: Order with out-of-stock item
    Given my cart contains:
      | Product | Quantity |
      | Laptop  | 15       |
    When I proceed to checkout
    Then I should see an "insufficient stock" error
    And my cart should remain unchanged

Benefits

BenefitDescription
Clear CommunicationBusiness language in specs
Living DocumentationTests are always up-to-date
Shared UnderstandingCommon language for all team members
Focus on ValueTests describe business value
Early DetectionCatch issues early

Best Practices

  1. One Scenario per Behavior: Each test should cover one behavior
  2. Use Meaningful Names: Scenario names should describe the behavior
  3. Keep Steps Simple: Reuse steps across scenarios
  4. Automate Everything: Run BDD tests in CI/CD
  5. Review Together: Team review of feature files

Tools Comparison

ToolLanguageUse Case
SpecFlowC#.NET applications
CucumberJava/RubyMulti-language
BehavePythonPython applications
JasmineJavaScriptJavaScript apps

References

Kiến trúc Hệ thống

Overview

System Architecture là thiết kế tổng thể của hệ thống, bao gồm cách các components tổ chức, giao tiếp, và tương tác với nhau. Nó định nghĩa cấu trúc, behavior, và mối quan hệ giữa các phần của hệ thống.

Types of System Architectures

1. Monolithic Architecture

Toàn bộ ứng dụng chạy trong một process duy nhất.

// Single deployment unit
public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }
    
    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();  // All in one
            });
}

2. Client-Server Architecture

Phân chia giữa client (frontend) và server (backend).

┌─────────────┐         ┌─────────────┐
│   Client    │────────▶│   Server    │
│  (Browser)  │◀────────│  (API)      │
└─────────────┘         └─────────────┘

3. Microservices Architecture

Chia ứng dụng thành các services nhỏ, độc lập.

┌────────────┐  ┌────────────┐  ┌────────────┐
│   Order   │  │   Product  │  │   User    │
│  Service  │  │  Service   │  │  Service  │
└─────┬──────┘  └─────┬──────┘  └─────┬──────┘
      │               │               │
      └───────────────┼───────────────┘
                     │
              ┌──────┴──────┐
              │ API Gateway │
              └─────────────┘
// Service A - Orders
public class OrderService : IOrderService
{
    public async Task<Order> GetOrderAsync(Guid id)
    {
        // Only handles order logic
    }
}

// Service B - Products (separate deployment)
public class ProductService : IProductService
{
    public async Task<Product> GetProductAsync(Guid id)
    {
        // Only handles product logic
    }
}

4. Event-Driven Architecture

Components giao tiếp qua events.

Producer ──Events──▶ Broker ──Events──▶ Consumers

5. Layered Architecture

┌────────────────────┐
│  Presentation      │  (Controllers, UI)
├────────────────────┤
│  Application       │  (Services, Use Cases)
├────────────────────┤
│  Domain           │  (Entities, Business Rules)
├────────────────────┤
│  Infrastructure    │  (Repositories, External APIs)
└────────────────────┘

Architectural Patterns

CQRS Pattern

// Separate read and write models
public interface ICommandHandler<TCommand> { }
public interface IQueryHandler<TQuery, TResult> { }

Repository Pattern

public interface IRepository<T>
{
    Task<T> GetByIdAsync(Guid id);
    Task<IEnumerable<T>> GetAllAsync();
    Task AddAsync(T entity);
    Task UpdateAsync(T entity);
    Task DeleteAsync(T entity);
}

Unit of Work Pattern

public interface IUnitOfWork
{
    Task SaveChangesAsync();
    IRepository<Order> Orders { get; }
    IRepository<Product> Products { get; }
    IRepository<Customer> Customers { get; }
}

Design Principles

1. High Cohesion, Low Coupling

// High cohesion - related things together
public class Order
{
    public Guid Id { get; private set; }
    public List<OrderItem> Items { get; private set; }
    public decimal Total => Items.Sum(i => i.Price * i.Quantity);
}

// Low coupling - depend on abstractions
public class OrderService
{
    private readonly IOrderRepository _repository;
    private readonly INotificationService _notification;
    
    public OrderService(
        IOrderRepository repository,
        INotificationService notification)
    {
        _repository = repository;
        _notification = notification;
    }
}

2. Dependency Inversion

// Depend on abstractions, not concretions
public interface IOrderRepository { }

public class OrderService
{
    private readonly IOrderRepository _repository;  // Interface
}

public class EfOrderRepository : IOrderRepository { }  // Implementation

3. Single Responsibility

// Each class has one reason to change
public class Order { }  // Only represents order data
public class OrderService { }  // Only handles order logic
public class OrderRepository { }  // Only handles data access

Scalability Patterns

1. Horizontal vs Vertical Scaling

// Vertical Scaling - bigger machine
services.AddSingleton<IHostApplicationLifetime>(host => host);

// Horizontal Scaling - more machines
// Load balancer distributes requests

2. Caching

// Distributed cache
services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "localhost:6379";
});

// Using cache
public async Task<Product> GetProductAsync(Guid id)
{
    var cacheKey = $"product:{id}";
    var cached = await _cache.GetAsync<Product>(cacheKey);
    
    if (cached != null)
        return cached;
    
    var product = await _repository.GetByIdAsync(id);
    await _cache.SetAsync(cacheKey, product);
    
    return product;
}

3. Database Sharding

// Sharding by user ID
public class ShardedOrderRepository : IOrderRepository
{
    public async Task<Order> GetByIdAsync(Guid id)
    {
        var shardId = GetShardId(id);
        var context = GetContextForShard(shardId);
        
        return await context.Orders.FindAsync(id);
    }
}

Reliability Patterns

1. Circuit Breaker

public class CircuitBreaker
{
    private int _failureCount;
    private const int Threshold = 5;
    private CircuitState _state = CircuitState.Closed;
    
    public async Task<T> ExecuteAsync<T>(Func<Task<T>> action)
    {
        if (_state == CircuitState.Open)
            throw new CircuitOpenException();
            
        try
        {
            var result = await action();
            _failureCount = 0;
            return result;
        }
        catch
        {
            _failureCount++;
            if (_failureCount >= Threshold)
                _state = CircuitState.Open;
            throw;
        }
    }
}

2. Retry with Exponential Backoff

public async Task<T> ExecuteWithRetryAsync<T>(Func<Task<T>> action)
{
    var retryCount = 0;
    var maxRetries = 3;
    
    while (retryCount < maxRetries)
    {
        try
        {
            return await action();
        }
        catch (Exception ex)
        {
            retryCount++;
            if (retryCount >= maxRetries)
                throw;
                
            var delay = TimeSpan.FromSeconds(Math.Pow(2, retryCount));
            await Task.Delay(delay);
        }
    }
    
    throw new Exception("Max retries exceeded");
}

Communication Patterns

1. Synchronous (REST/gRPC)

// REST API
[HttpGet("{id}")]
public async Task<Order> GetOrder(Guid id)
{
    return await _orderService.GetOrderAsync(id);
}

// gRPC
public class OrderService : OrderServiceBase
{
    public override async Task<OrderResponse> GetOrder(
        OrderRequest request, ServerCallContext context)
    {
        return await _orderRepository.GetByIdAsync(Guid.Parse(request.Id));
    }
}

2. Asynchronous (Message Queue)

// Publish event
await _messageBus.PublishAsync(new OrderCreatedEvent(order));

// Subscribe to events
public class OrderCreatedHandler : IMessageHandler<OrderCreatedEvent>
{
    public async Task Handle(OrderCreatedEvent message)
    {
        await _notificationService.SendAsync(message.CustomerEmail);
    }
}

Monitoring and Observability

// Health checks
public class OrderServiceHealthCheck : IHealthCheck
{
    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context, CancellationToken cancellationToken)
    {
        var canConnect = await _repository.CanConnectAsync();
        
        return canConnect 
            ? HealthCheckResult.Healthy("Order service is healthy")
            : HealthCheckResult.Unhealthy("Cannot connect to database");
    }
}

// Metrics
public class OrderMetrics
{
    private readonly Counter _orderCount;
    
    public OrderMetrics(IMeterFactory meterFactory)
    {
        _orderCount = meterFactory.CreateCounter("orders.created");
    }
    
    public void RecordOrderCreated() => _orderCount.Add(1);
}

Choosing Right Architecture

FactorRecommended Architecture
Small team, simple appMonolithic
Large team, complex domainMicroservices
Real-time processingEvent-Driven
High read/write ratioCQRS
Frequent changesModular Monolith

References

Microservices

Nguyên tắc Microservice

Đặc điểm chính

CharacteristicDescription
Single ResponsibilityMỗi service chỉ làm một việc
Loose CouplingServices giao tiếp qua APIs
Independent DeployDeploy không ảnh hưởng services khác
Technology DiversityMỗi service có thể dùng công nghệ khác
OwnershipTeam sở hữu service từ dev đến production

Monolith vs Microservices

┌─────────────────────────────────┐     ┌───────┐  ┌───────┐  ┌───────┐
│          MONOLITH               │     │Order  │  │Product│  │Customer│
│  ┌───────────────────────────┐  │     │Service│  │Service│  │Service │
│  │ UI │ Business │ Data │    │  │     └───┬───┘  └───┬───┘  └───┬───┘
│  └───────────────────────────┘  │         │          │          │
└─────────────────────────────────┘         └──────────┴──────────┘
                                          ┌─────────────────────────┐
                                          │     API GATEWAY         │
                                          └─────────────────────────┘

API Gateway (Ocelot)

Cài đặt

dotnet add package Ocelot

Cấu hình ocelot.json

{
  "Routes": [
    {
      "DownstreamPathTemplate": "/api/products/{everything}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        { "Host": "product-service", "Port": 80 }
      ],
      "UpstreamPathTemplate": "/products/{everything}",
      "UpstreamHttpMethod": ["GET", "POST", "PUT", "DELETE"],
      "RateLimitOptions": {
        "ClientWhitelist": "",
        "EnableRateLimiting": true,
        "Period": "1s",
        "PeriodTimespan": 1,
        "Limit": 100
      },
      "AuthenticationOptions": {
        "AuthenticationProviderKey": "Bearer",
        "AllowedScopes": []
      }
    },
    {
      "DownstreamPathTemplate": "/api/orders/{everything}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        { "Host": "order-service", "Port": 80 }
      ],
      "UpstreamPathTemplate": "/orders/{everything}",
      "UpstreamHttpMethod": ["GET", "POST", "PUT", "DELETE"]
    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "http://localhost:5000"
  }
}

Program.cs

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOcelot();

var app = builder.Build();

await app.UseOcelot();

app.Run();

Message Bus

RabbitMQ

Cài đặt

dotnet add package RabbitMQ.Client

Producer

public class OrderMessagePublisher
{
    private readonly IConnection _connection;
    private readonly IModel _channel;
    
    public OrderMessagePublisher()
    {
        var factory = new ConnectionFactory
        {
            HostName = "rabbitmq",
            UserName = "guest",
            Password = "guest"
        };
        
        _connection = factory.CreateConnection();
        _channel = _connection.CreateModel();
        
        _channel.ExchangeDeclare("orders", ExchangeType.Direct, durable: true);
    }
    
    public void PublishOrderCreated(Order order)
    {
        var message = JsonSerializer.Serialize(order);
        var body = Encoding.UTF8.GetBytes(message);
        
        var properties = _channel.CreateBasicProperties();
        properties.Persistent = true;
        properties.ContentType = "application/json";
        
        _channel.BasicPublish(
            exchange: "orders",
            routingKey: "order.created",
            basicProperties: properties,
            body: body);
    }
}

Consumer

public class OrderMessageConsumer : BackgroundService
{
    private readonly IConnection _connection;
    private readonly IModel _channel;
    
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var factory = new ConnectionFactory
        {
            HostName = "rabbitmq",
            UserName = "guest",
            Password = "guest"
        };
        
        _connection = factory.CreateConnection();
        _channel = _connection.CreateModel();
        
        _channel.QueueDeclare("order.created", durable: true);
        
        var consumer = new EventingBasicConsumer(_channel);
        consumer.Received += async (model, ea) =>
        {
            var body = ea.Body.ToArray();
            var message = Encoding.UTF8.GetString(body);
            
            var order = JsonSerializer.Deserialize<Order>(message);
            await ProcessOrderAsync(order);
            
            _channel.BasicAck(ea.DeliveryTag, false);
        };
        
        _channel.BasicConsume(
            queue: "order.created",
            autoAck: false,
            consumer: consumer);
        
        while (!stoppingToken.IsCancellationRequested)
        {
            await Task.Delay(1000, stoppingToken);
        }
    }
}

Kafka

Cài đặt

dotnet add package Confluent.Kafka

Producer

var config = new ProducerConfig
{
    BootstrapServers = "localhost:9092",
    ClientId = "order-producer"
};

using var producer = new ProducerBuilder<string, string>(config).Build();

var message = new Message<string, string>
{
    Key = order.Id.ToString(),
    Value = JsonSerializer.Serialize(order)
};

var result = await producer.ProduceAsync("orders", message);

Consumer

var config = new ConsumerConfig
{
    BootstrapServers = "localhost:9092",
    GroupId = "order-consumer",
    AutoOffsetReset = AutoOffsetReset.Earliest
};

using var consumer = new ConsumerBuilder<string, string>(config).Build();

consumer.Subscribe("orders");

while (true)
{
    var consumeResult = consumer.Consume();
    var order = JsonSerializer.Deserialize<Order>(consumeResult.Message.Value);
    await ProcessOrderAsync(order);
}

RabbitMQ vs Kafka

AspectRabbitMQKafka
ProtocolAMQPBinary (custom)
Use CaseTask queues, simple messagingEvent streaming, high throughput
Message RetentionPer-queue (short-term)Topic-based (long-term)
OrderingPer-queuePer-partition
ScalabilityHorizontalVery high
ComplexitySimplerMore complex
Delivery GuaranteeAt-least-once, Exactly-onceAt-least-once, Exactly-once

Monolithic Architecture

Overview

Monolithic Architecture là kiến trúc truyền thống trong đó toàn bộ ứng dụng được xây dựng như một đơn vị duy nhất. Tất cả components - UI, business logic, data access - đều nằm trong một codebase và được deploy cùng nhau.

Characteristics

┌─────────────────────────────────────────────────┐
│                 Web Server                       │
├─────────────────────────────────────────────────┤
│                  UI Layer                        │
│  ┌─────────────────────────────────────────┐   │
│  │           Controllers/Views             │   │
│  └─────────────────────────────────────────┘   │
├─────────────────────────────────────────────────┤
│              Business Logic Layer              │
│  ┌─────────────────────────────────────────┐   │
│  │     Services, Business Rules            │   │
│  └─────────────────────────────────────────┘   │
├─────────────────────────────────────────────────┤
│                Data Access Layer                │
│  ┌─────────────────────────────────────────┐   │
│  │     Repositories, ORM, SQL              │   │
│  └─────────────────────────────────────────┘   │
├─────────────────────────────────────────────────┤
│                  Database                        │
└─────────────────────────────────────────────────┘

Structure

// Monolithic Project Structure
MyApp/
├── Controllers/
│   ├── ProductsController.cs
│   ├── OrdersController.cs
│   └── CustomersController.cs
├── Services/
│   ├── ProductService.cs
│   ├── OrderService.cs
│   └── CustomerService.cs
├── Models/
│   ├── Product.cs
│   ├── Order.cs
│   └── Customer.cs
├── Repositories/
│   ├── ProductRepository.cs
│   ├── OrderRepository.cs
│   └── CustomerRepository.cs
├── Views/
│   ├── Products/
│   ├── Orders/
│   └── Customers/
└── AppDbContext.cs

Implementation Example

// Controllers/OrdersController.cs
public class OrdersController : Controller
{
    private readonly OrderService _orderService;
    
    public OrdersController(OrderService orderService)
    {
        _orderService = orderService;
    }
    
    [HttpGet]
    public async Task<IActionResult> Index()
    {
        var orders = await _orderService.GetAllOrdersAsync();
        return View(orders);
    }
    
    [HttpPost]
    public async Task<IActionResult> Create(CreateOrderViewModel model)
    {
        if (!ModelState.IsValid)
            return View(model);
            
        await _orderService.CreateOrderAsync(model);
        return RedirectToAction(nameof(Index));
    }
}

// Services/OrderService.cs
public class OrderService
{
    private readonly OrderRepository _orderRepository;
    private readonly ProductRepository _productRepository;
    private readonly EmailService _emailService;
    
    public OrderService(
        OrderRepository orderRepository,
        ProductRepository productRepository,
        EmailService emailService)
    {
        _orderRepository = orderRepository;
        _productRepository = productRepository;
        _emailService = emailService;
    }
    
    public async Task CreateOrderAsync(CreateOrderViewModel model)
    {
        var order = new Order
        {
            CustomerId = model.CustomerId,
            OrderDate = DateTime.Now,
            Status = OrderStatus.Pending
        };
        
        foreach (var item in model.Items)
        {
            var product = await _productRepository.GetByIdAsync(item.ProductId);
            order.AddItem(product, item.Quantity);
        }
        
        await _orderRepository.SaveAsync(order);
        
        await _emailService.SendOrderConfirmation(order);
    }
}

// Repositories/OrderRepository.cs
public class OrderRepository
{
    private readonly AppDbContext _context;
    
    public OrderRepository(AppDbContext context)
    {
        _context = context;
    }
    
    public async Task SaveAsync(Order order)
    {
        _context.Orders.Add(order);
        await _context.SaveChangesAsync();
    }
}

Advantages

AdvantageDescription
SimplicityEasy to develop và debug
PerformanceIn-process calls are fast
ConsistencySingle codebase, shared resources
TransactionEasy to maintain ACID transactions
DeploymentSimple deployment (single package)
DevelopmentGood for small teams

Disadvantages

DisadvantageDescription
ScalabilityKhó scale theo component
TechnologyHard to adopt new technologies
ReliabilityOne failure affects entire app
DeploymentPhải redeploy toàn bộ app
DevelopmentCodebase grows large, hard to maintain
CouplingTightly coupled components

When to Use

  • Small to medium applications
  • Teams mới hoặc limited experience
  • Rapid prototyping
  • Applications với tight integration requirements
  • Projects với limited scope

When NOT to Use

  • Large, complex applications
  • Applications requiring frequent scaling
  • Teams cần technological flexibility
  • Microservices-based requirements

Scaling Strategies

Vertical Scaling

// Load balancing configuration
services.AddLoadBalancing(options =>
{
    options.MaxParallelRequests = 10;
});

Horizontal Scaling (Multiple Instances)

// Using sticky sessions or session state
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
    options.Cookie.Name = ".MyApp.Session";
    options.IdleTimeout = TimeSpan.FromMinutes(20);
});

Database Scaling

// Read/Write splitting
public class OrderRepository
{
    private readonly AppDbContext _readContext;
    private readonly AppDbContext _writeContext;
    
    public async Task<List<Order>> GetOrdersAsync()
    {
        return await _readContext.Orders.ToListAsync();
    }
    
    public async Task SaveAsync(Order order)
    {
        await _writeContext.Orders.AddAsync(order);
        await _writeContext.SaveChangesAsync();
    }
}

Modern Monolithic Approaches

// Modular Monolith - Separate by feature
src/
├── Modules/
│   ├── Orders/
│   │   ├── OrdersController.cs
│   │   ├── OrderService.cs
│   │   ├── OrderRepository.cs
│   │   └── OrdersModule.cs  // Module registration
│   ├── Products/
│   │   ├── ProductsController.cs
│   │   ├── ProductService.cs
│   │   ├── ProductRepository.cs
│   │   └── ProductsModule.cs
│   └── Customers/
│       ├── CustomersController.cs
│       ├── CustomerService.cs
│       ├── CustomerRepository.cs
│       └── CustomersModule.cs
└── Shared/
    ├── Database/
    ├── Logging/
    └── Authentication/

Best Practices

  1. Use Layers: Separate concerns within the monolith
  2. Modularize: Group code by feature, not by technical layer
  3. Configuration: Externalize configuration
  4. Logging: Implement centralized logging
  5. Monitoring: Add health checks và metrics

Migration Path

Monolithic → Modular Monolithic → Microservices
    ↓              ↓                    ↓
 Single app    Feature modules     Distributed
                                             

References

Serverless Architecture

Overview

Serverless Architecture là mô hình trong đó developers không cần quản lý servers. Cloud provider tự động cung cấp, scale, và quản lý compute resources. Bạn chỉ tập trung vào code và trả tiền cho thời gian compute thực sự được sử dụng.

Key Concepts

1. Function as a Service (FaaS)

// AWS Lambda function
public class FunctionHandler
{
    public async Task<APIGatewayProxyResponse> Handle(
        APIGatewayProxyRequest request, 
        ILambdaContext context)
    {
        context.Logger.LogLine("Processing request");
        
        var response = new
        {
            message = "Hello from serverless!",
            timestamp = DateTime.UtcNow
        };
        
        return new APIGatewayProxyResponse
        {
            StatusCode = 200,
            Body = JsonSerializer.Serialize(response),
            Headers = new Dictionary<string, string>
            {
                { "Content-Type", "application/json" }
            }
        };
    }
}

2. Serverless Compute Platforms

  • AWS Lambda
  • Azure Functions
  • Google Cloud Functions
  • Alibaba Cloud Function Compute

Architecture Pattern

┌─────────────────────────────────────────────────────┐
│                    Client Apps                       │
│   (Web, Mobile, IoT)                                │
└─────────────────────────┬───────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────┐
│              API Gateway                             │
│   (Authentication, Rate Limiting, Routing)           │
└─────────────────────────┬───────────────────────────┘
                          │
         ┌────────────────┼────────────────┐
         ▼                ▼                ▼
   ┌──────────┐     ┌──────────┐     ┌──────────┐
   │ Function │     │ Function │     │ Function │
   │  (Auth)  │     │ (Order)  │     │ (Payment)│
   └────┬─────┘     └────┬─────┘     └────┬─────┘
        │                │                │
        └────────────────┼────────────────┘
                         │
         ┌───────────────┼───────────────┐
         ▼               ▼               ▼
   ┌──────────┐   ┌──────────┐   ┌──────────┐
   │   S3     │   │   DynamoDB│   │   SNS    │
   │ (Files)  │   │   (DB)    │   │  (Events)│
   └──────────┘   └──────────┘   └──────────┘

AWS Lambda Example

// Order Processing Function
public class OrderProcessor
{
    private readonly IOrderRepository _orderRepository;
    private readonly IEmailService _emailService;
    
    public OrderProcessor(IOrderRepository orderRepository, IEmailService emailService)
    {
        _orderRepository = orderRepository;
        _emailService = emailService;
    }
    
    [LambdaFunction(Role = "order-processor-role")]
    public async Task HandleOrderCreated(
        [SQSTrigger("order-queue")] OrderMessage message,
        ILambdaContext context)
    {
        context.Logger.LogLine($"Processing order: {message.OrderId}");
        
        var order = await _orderRepository.GetAsync(message.OrderId);
        
        if (order != null)
        {
            await _emailService.SendConfirmationAsync(order);
            context.Logger.LogLine("Order processed successfully");
        }
    }
}

// S3 Event Trigger
public class ImageProcessor
{
    [LambdaFunction(Role = "image-processor-role")]
    public async Task ProcessImage(
        [S3Trigger("my-bucket")] S3EventNotification event,
        ILambdaContext context)
    {
        foreach (var record in event.Records)
        {
            var key = record.S3.Object.Key;
            context.Logger.LogLine($"Processing image: {key}");
            
            // Process image (resize, compress, etc.)
            await ProcessImageAsync(key);
        }
    }
}

Azure Functions Example

// Timer-triggered function
public class ScheduledTask
{
    [FunctionName("DailyCleanup")]
    public async Task Run(
        [TimerTrigger("0 0 2 * * *")] TimerInfo timer,
        ILogger log)
    {
        log.LogInformation($"Function executed at: {DateTime.Now}");
        
        // Cleanup old records
        await CleanupOldRecordsAsync();
    }
}

// HTTP-triggered function with CosmosDB binding
public class ProductFunctions
{
    [FunctionName("GetProducts")]
    public async Task<IActionResult> GetProducts(
        [HttpTrigger(AuthorizationLevel.Function, "get", Route = "products")] 
        HttpRequest req,
        [CosmosDB("db", "products", ConnectionStringSetting = "CosmosDB")] 
        IEnumerable<Product> products,
        ILogger log)
    {
        log.LogInformation("Fetching products from CosmosDB");
        return new OkObjectResult(products);
    }
    
    [FunctionName("CreateProduct")]
    public async Task<IActionResult> CreateProduct(
        [HttpTrigger(AuthorizationLevel.Function, "post", Route = "products")] 
        HttpRequest req,
        [CosmosDB("db", "products", ConnectionStringSetting = "CosmosDB")] 
        out dynamic document,
        ILogger log)
    {
        var requestBody = await new StreamReader(req.Body).ReadToEndAsync();
        document = JsonSerializer.Deserialize<dynamic>(requestBody);
        return new CreatedResult($"/api/products/{document.id}", document);
    }
}

Infrastructure as Code

# serverless.yml (Serverless Framework)
service: my-serverless-app

provider:
  name: aws
  runtime: dotnet6
  memorySize: 256
  timeout: 30
  environment:
    TABLE_NAME: ${self:service}-${opt:stage, 'dev'}

functions:
  hello:
    handler: CsharpHandlers::Hello.Handler
    events:
      - http:
          path: /hello
          method: get
  
  createOrder:
    handler: CsharpHandlers::Orders.Create
    events:
      - http:
          path: /orders
          method: post
      - sqs:
          arn: !GetAtt OrderQueue.Arn

resources:
  Resources:
    OrderQueue:
      Type: AWS::SQS::Queue
      Properties:
        QueueName: ${self:service}-orders-queue

plugins:
  - serverless-dynamodb-plugin
  - serverless-offline

Benefits

BenefitDescription
No Server ManagementNo OS, patching, or capacity planning
Auto-scalingAutomatic scaling from 0 to thousands
Pay per usePay only for compute time used
Faster time to marketFocus on business logic
Reduced operational overheadCloud provider handles infrastructure
Built-in high availabilityMultiple AZ redundancy

Challenges

ChallengeDescription
Cold startsInitial invocation latency
Vendor lock-inPlatform-specific APIs
Testing complexityHard to test locally
StatelessnessMust handle state externally
DebuggingDistributed tracing complexity
SecurityNeed to secure function configurations

Best Practices

1. Stateless Functions

// Don't store state in function
// Use external services instead
public class FunctionHandler
{
    public async Task<Response> Handle(Request request)
    {
        // Get state from cache/database
        var cache = await _cacheService.GetAsync(request.UserId);
        
        // Process
        var result = await _service.ProcessAsync(cache);
        
        return result;
    }
}

2. Proper Configuration

public class FunctionHandler
{
    public async Task<Response> Handle(Request request)
    {
        var maxRetries = Environment.GetEnvironmentVariable("MAX_RETRIES")
            ?? "3";
        
        var connectionString = Environment.GetEnvironmentVariable("DB_CONNECTION");
        
        // Use configuration safely
    }
}

3. Idempotency

public async Task Handle(OrderCreatedEvent evt, ILambdaContext context)
{
    // Check if already processed
    var isProcessed = await _redis.SetIfNotExistsAsync(
        $"order:{evt.OrderId}:processed",
        TimeSpan.FromDays(1));
    
    if (!isProcessed)
    {
        context.Logger.LogLine("Order already processed, skipping");
        return;
    }
    
    // Process order
    await ProcessOrderAsync(evt);
}

4. Error Handling

public async Task Handle(Request request, ILambdaContext context)
{
    try
    {
        await ProcessAsync(request);
    }
    catch (Exception ex)
    {
        context.Logger.LogError(ex, "Error processing request");
        
        // Send to dead letter queue
        await _dlq.SendAsync(new DeadLetterMessage
        {
            Request = request,
            Error = ex.Message,
            Timestamp = DateTime.UtcNow
        });
        
        throw; // Re-throw for retry
    }
}

Use Cases

Use CaseDescription
Web backendsAPIs và web applications
Data processingETL, batch processing
Real-time file processingImage resizing, video transcoding
IoT backendsIoT data ingestion
ChatbotsNLP và message handling
Scheduled tasksCron jobs, cleanup tasks
Event-driven appsEvent handling, notifications

Comparison

AspectServerlessTraditional
Server managementProviderSelf
ScalingAutomaticManual/auto
PricingPer executionAlways on
DeploymentFunctionsContainers/servers
LatencyCold startsAlways ready
ComplexityDistributedMonolithic

References

Event Sourcing

Overview

Event Sourcing là một pattern lưu trữ trạng thái của một application như một chuỗi các events thay vì chỉ lưu trữ current state. Thay vì lưu “what” (current state), chúng ta lưu trữ “what happened” (tất cả các thay đổi).

Core Concept

Traditional Approach:
┌──────────────────────────────────┐
│     Current State Only           │
│  ┌────────────────────────────┐  │
│  │   Account Balance: $1000  │  │
│  └────────────────────────────┘  │
└──────────────────────────────────┘

Event Sourcing:
┌──────────────────────────────────┐
│     Event Store                  │
│  ┌────────────────────────────┐  │
│  │ AccountCreated             │  │
│  │ Deposited: $500             │  │
│  │ Deposited: $300             │  │
│  │ Withdrew: $100              │  │
│  │ Withdrew: $200              │  │
│  │ Deposited: $500            │  │
│  └────────────────────────────┘  │
└──────────────────────────────────┘
   Current Balance: $1000

Key Terms

  • Event: Something that happened in the system
  • Event Store: Database lưu trữ events
  • Aggregate: Entity whose state is derived from events
  • Projection: Way to materialize state from events
  • Snapshot: Cached state để avoid replaying all events

Implementation

1. Events Definition

// Base event class
public abstract class Event
{
    public Guid Id { get; } = Guid.NewGuid();
    public DateTime Timestamp { get; } = DateTime.UtcNow;
    public int Version { get; set; }
}

// Domain events
public class AccountCreatedEvent : Event
{
    public Guid AccountId { get; }
    public string OwnerName { get; }
    public string Email { get; }
    
    public AccountCreatedEvent(Guid accountId, string ownerName, string email)
    {
        AccountId = accountId;
        OwnerName = ownerName;
        Email = email;
    }
}

public class MoneyDepositedEvent : Event
{
    public Guid AccountId { get; }
    public decimal Amount { get; }
    public string Description { get; }
    
    public MoneyDepositedEvent(Guid accountId, decimal amount, string description)
    {
        AccountId = accountId;
        Amount = amount;
        Description = description;
    }
}

public class MoneyWithdrawnEvent : Event
{
    public Guid AccountId { get; }
    public decimal Amount { get; }
    public string Description { get; }
    
    public MoneyWithdrawnEvent(Guid accountId, decimal amount, string description)
    {
        AccountId = accountId;
        Amount = amount;
        Description = description;
    }
}

2. Aggregate

public class BankAccount : AggregateRoot
{
    public Guid Id { get; private set; }
    public string OwnerName { get; private set; }
    public string Email { get; private set; }
    public decimal Balance { get; private set; }
    
    private readonly List<Event> _pendingEvents = new();
    
    // For reconstruction from event store
    public BankAccount() { }
    
    // Factory method
    public static BankAccount Create(string ownerName, string email)
    {
        var account = new BankAccount
        {
            Id = Guid.NewGuid(),
            OwnerName = ownerName,
            Email = email,
            Balance = 0
        };
        
        account._pendingEvents.Add(
            new AccountCreatedEvent(account.Id, ownerName, email));
        
        return account;
    }
    
    // Apply events (for replay)
    public void Apply(Event evt)
    {
        switch (evt)
        {
            case AccountCreatedEvent created:
                Id = created.AccountId;
                OwnerName = created.OwnerName;
                Email = created.Email;
                break;
                
            case MoneyDepositedEvent deposited:
                Balance += deposited.Amount;
                break;
                
            case MoneyWithdrawnEvent withdrawn:
                Balance -= withdrawn.Amount;
                break;
        }
    }
    
    public void Deposit(decimal amount, string description)
    {
        if (amount <= 0)
            throw new InvalidOperationException("Amount must be positive");
            
        var evt = new MoneyWithdrawnEvent(Id, amount, description);
        Apply(evt);
        _pendingEvents.Add(evt);
    }
    
    public void Withdraw(decimal amount, string description)
    {
        if (amount <= 0)
            throw new InvalidOperationException("Amount must be positive");
            
        if (amount > Balance)
            throw new InvalidOperationException("Insufficient balance");
            
        var evt = new MoneyWithdrawnEvent(Id, amount, description);
        Apply(evt);
        _pendingEvents.Add(evt);
    }
    
    public IReadOnlyList<Event> GetPendingEvents() => _pendingEvents.AsReadOnly();
}

3. Event Store

public interface IEventStore
{
    Task SaveAsync(Guid aggregateId, IEnumerable<Event> events);
    Task<List<Event>> GetEventsAsync(Guid aggregateId, int fromVersion = 0);
}

public class EventStore : IEventStore
{
    private readonly DbContext _context;
    
    public async Task SaveAsync(Guid aggregateId, IEnumerable<Event> events)
    {
        foreach (var evt in events)
        {
            var storedEvent = new StoredEvent
            {
                Id = evt.Id,
                AggregateId = aggregateId,
                EventType = evt.GetType().Name,
                Data = JsonSerializer.Serialize(evt),
                Timestamp = evt.Timestamp,
                Version = evt.Version
            };
            
            await _context.Events.AddAsync(storedEvent);
        }
        
        await _context.SaveChangesAsync();
    }
    
    public async Task<List<Event>> GetEventsAsync(Guid aggregateId, int fromVersion = 0)
    {
        var storedEvents = await _context.Events
            .Where(e => e.AggregateId == aggregateId && e.Version > fromVersion)
            .OrderBy(e => e.Version)
            .ToListAsync();
            
        return storedEvents.Select(Deserialize).ToList();
    }
    
    private Event Deserialize(StoredEvent stored)
    {
        var type = Type.GetType(stored.EventType);
        return JsonSerializer.Deserialize(stored.Data, type) as Event;
    }
}

4. Repository

public interface IRepository<T> where T : AggregateRoot
{
    Task<T> GetByIdAsync(Guid id);
    Task SaveAsync(T aggregate);
}

public class BankAccountRepository : IRepository<BankAccount>
{
    private readonly IEventStore _eventStore;
    
    public BankAccountRepository(IEventStore eventStore)
    {
        _eventStore = eventStore;
    }
    
    public async Task<BankAccount> GetByIdAsync(Guid id)
    {
        var events = await _eventStore.GetEventsAsync(id);
        
        var account = new BankAccount();
        foreach (var evt in events)
        {
            account.Apply(evt);
        }
        
        return account;
    }
    
    public async Task SaveAsync(BankAccount account)
    {
        var events = account.GetPendingEvents();
        
        if (events.Any())
        {
            await _eventStore.SaveAsync(account.Id, events);
            account.ClearPendingEvents();
        }
    }
}

Projections

Projections tạo ra views từ events cho việc đọc.

// Simple projection - build current state
public class AccountProjection
{
    public static BankAccount Project(IEnumerable<Event> events)
    {
        var account = new BankAccount();
        foreach (var evt in events)
        {
            account.Apply(evt);
        }
        return account;
    }
}

// Multiple projections for different purposes
public class AccountSummaryProjection
{
    private readonly Dictionary<Guid, AccountSummary> _summaries = new();
    
    public void Project(Event evt)
    {
        switch (evt)
        {
            case AccountCreatedEvent created:
                _summaries[created.AccountId] = new AccountSummary
                {
                    AccountId = created.AccountId,
                    OwnerName = created.OwnerName,
                    Balance = 0
                };
                break;
                
            case MoneyDepositedEvent deposited:
                _summaries[deposited.AccountId].Balance += deposited.Amount;
                break;
                
            case MoneyWithdrawnEvent withdrawn:
                _summaries[withdrawn.AccountId].Balance -= withdrawn.Amount;
                break;
        }
    }
    
    public AccountSummary GetSummary(Guid accountId)
        => _summaries.GetValueOrDefault(accountId);
}

Snapshots

public interface ISnapshotStore
{
    Task SaveAsync<T>(Guid aggregateId, int version, T snapshot);
    Task<T> GetAsync<T>(Guid aggregateId);
}

public class SnapshottingRepository<T> : IRepository<T> where T : AggregateRoot
{
    private readonly IEventStore _eventStore;
    private readonly ISnapshotStore _snapshotStore;
    private const int SnapshotInterval = 100;
    
    public async Task<T> GetByIdAsync(Guid id)
    {
        // Try to get snapshot first
        var snapshot = await _snapshotStore.GetAsync<T>(id);
        var fromVersion = 0;
        
        if (snapshot != null)
        {
            fromVersion = snapshot.Version;
        }
        
        // Get events after snapshot
        var events = await _eventStore.GetEventsAsync(id, fromVersion);
        
        // Replay
        var aggregate = snapshot?.Data ?? CreateAggregate();
        
        foreach (var evt in events)
        {
            aggregate.Apply(evt);
        }
        
        return aggregate;
    }
}

Benefits

BenefitDescription
Complete AuditFull history of all changes
Temporal QueriesQuery state at any point in time
Event ReplayRecreate state by replaying events
DebuggingReplay events to understand bugs
ScalabilityAppend-only event store
FlexibilityEasy to add new projections

Challenges

ChallengeDescription
ComplexityMore complex than traditional CRUD
Learning CurveHarder to understand
Event SchemaChanges require migration strategies
StorageCan grow large (need snapshots)
ConsistencyEventual consistency

Use Cases

Use CaseDescription
BankingComplete transaction history
AuditFull audit trail requirements
CollaborationActivity feeds
E-commerceOrder processing
GamingGame state management
IoTEvent logging

Comparison

AspectTraditionalEvent Sourcing
StorageCurrent stateHistory of changes
AuditAdd audit tablesBuilt-in
QueriesQuery current stateProject from events
ComplexityLowerHigher
DebuggingHarderReplay events

References

6. Hiệu suất và Xử lý Bất đồng bộ

Giới thiệu

Phần này trình bày các kỹ thuật tối ưu hiệu suất và xử lý bất đồng bộ trong .NET.

Nội dung chính

Caching

Xử lý tải

Bất đồng bộ

Caching

In-Memory Cache

IMemoryCache

// Đăng ký
builder.Services.AddMemoryCache();

// Sử dụng
public class ProductService
{
    private readonly IMemoryCache _cache;
    private readonly AppDbContext _context;
    
    public ProductService(IMemoryCache cache, AppDbContext context)
    {
        _cache = cache;
        _context = context;
    }
    
    public async Task<List<Product>> GetProductsAsync()
    {
        // TryGetValue - Kiểm tra cache
        if (!_cache.TryGetValue("products", out List<Product> products))
        {
            // Load từ database
            products = await _context.Products.ToListAsync();
            
            // Set cache với options
            var options = new MemoryCacheEntryOptions()
                .SetSlidingExpiration(TimeSpan.FromMinutes(10))
                .SetAbsoluteExpiration(TimeSpan.FromHours(1))
                .SetPriority(CacheItemPriority.Normal)
                .SetSize(products.Count);
            
            _cache.Set("products", products, options);
        }
        
        return products;
    }
    
    public async Task<Product> GetProductByIdAsync(int id)
    {
        var key = $"product_{id}";
        
        return await _cache.GetOrCreateAsync(key, async entry =>
        {
            entry.SlidingExpiration = TimeSpan.FromMinutes(5);
            
            return await _context.Products.FindAsync(id);
        });
    }
}

Cache Options

var options = new MemoryCacheEntryOptions()
    // Thời gian cache không được access trước khi expire
    .SetSlidingExpiration(TimeSpan.FromMinutes(10))
    // Thời gian cache tồn tại tuyệt đối
    .SetAbsoluteExpiration(TimeSpan.FromHours(1))
    // Kết hợp cả hai
    .SetAbsoluteExpirationRelativeToNow(TimeSpan.FromHours(1))
    // Callback khi cache bị remove
    .RegisterPostEvictionCallback((key, value, reason, state) =>
    {
        Console.WriteLine($"Cache '{key}' removed: {reason}");
    });

Distributed Cache (Redis)

Cài đặt

dotnet add package StackExchange.Redis

Cấu hình

builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "localhost:6379";
    options.InstanceName = "MyApp:";
});

Sử dụng

public class RedisCacheService
{
    private readonly IDistributedCache _cache;
    
    public RedisCacheService(IDistributedCache cache)
    {
        _cache = cache;
    }
    
    // Set
    public async Task SetAsync<T>(string key, T value)
    {
        var json = JsonSerializer.Serialize(value);
        var bytes = Encoding.UTF8.GetBytes(json);
        
        await _cache.SetAsync(key, bytes, new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1),
            SlidingExpiration = TimeSpan.FromMinutes(10)
        });
    }
    
    // Get
    public async Task<T> GetAsync<T>(string key)
    {
        var bytes = await _cache.GetAsync(key);
        if (bytes == null) return default;
        
        var json = Encoding.UTF8.GetString(bytes);
        return JsonSerializer.Deserialize<T>(json);
    }
    
    // Remove
    public async Task RemoveAsync(string key)
    {
        await _cache.RemoveAsync(key);
    }
}

Response Caching

Cấu hình

// Program.cs
builder.Services.AddResponseCaching();

var app = builder.Build();
app.UseResponseCaching();

Sử dụng

[ApiController]
[Route("api/[controller]")]
[ResponseCache(Duration = 60, VaryByHeader = "Accept")]
public class ProductsController : ControllerBase
{
    [HttpGet]
    [ResponseCache(Duration = 120)]
    public IActionResult GetProducts()
    {
        return Ok(new { data = "cached" });
    }
}

Cache Headers

[HttpGet]
public IActionResult GetProducts()
{
    Response.Headers.CacheControl = "public, max-age=60";
    Response.Headers.Vary = "Accept-Encoding";
    
    return Ok();
}

Cache Strategies

Cache-Aside Pattern

┌─────────────────────────────────────────────────────────────────┐
│                      CACHE-ASIDE PATTERN                       │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   1. Request → Cache                                            │
│                         ↓                                       │
│   2. Cache hit? ──No──→ Database → Cache → Response            │
│        ↓                                                        │
│       Yes                                                        │
│        ↓                                                        │
│   3. Response (from cache)                                       │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘
public async Task<Product> GetProductAsync(int id)
{
    var key = $"product_{id}";
    
    // 1. Check cache
    var cached = await _cache.GetAsync<Product>(key);
    if (cached != null)
        return cached;
    
    // 2. Load from database
    var product = await _context.Products.FindAsync(id);
    if (product != null)
    {
        // 3. Store in cache
        await _cache.SetAsync(key, product, TimeSpan.FromMinutes(10));
    }
    
    return product;
}

Invalidate Cache

public async Task UpdateProductAsync(Product product)
{
    // Update database
    _context.Products.Update(product);
    await _context.SaveChangesAsync();
    
    // Invalidate cache
    await _cache.RemoveAsync($"product_{product.Id}");
}

Xử lý Tải

Rate Limiting

Cài đặt

dotnet add package AspNetCoreRateLimit

Cấu hình

// Program.cs
builder.Services.AddMemoryCache();
builder.Services.Configure<IpRateLimitOptions>(options =>
{
    options.EnableEndpointRateLimiting = true;
    options.StackBlockedRequests = false;
    options.GeneralRules = new List<RateLimitRule>
    {
        new RateLimitRule
        {
            Endpoint = "*",
            Period = "1m",
            Limit = 60
        },
        new RateLimitRule
        {
            Endpoint = "POST:/api/auth/login",
            Period = "1m",
            Limit = 5
        }
    };
});

builder.Services.AddInProcessMessageBus();
builder.Services.AddIpRateLimiting();

var app = builder.Build();
app.UseIpRateLimiting();

Controller

[HttpGet]
[EnableRateLimiting("PerUser")]
public IActionResult GetExpensiveData()
{
    return Ok();
}

Load Balancing

Round Robin

Request 1 → Instance 1
Request 2 → Instance 2
Request 3 → Instance 3
Request 4 → Instance 1 (loop)

Health Checks với Load Balancer

builder.Services.AddHealthChecks()
    .AddDbContextCheck<AppDbContext>("database")
    .AddRedis("localhost:6379", name: "redis");

var app = builder.Build();

// Health endpoint
app.MapHealthChecks("/health");

// Detailed health endpoint
app.MapHealthChecks("/health/detail", new HealthCheckOptions
{
    ResponseWriter = async (context, report) =>
    {
        context.ContentType = "application/json";
        await context.Response.WriteAsJsonAsync(new
        {
            status = report.Status.ToString(),
            checks = report.Entries.Select(e => new
            {
                name = e.Key,
                status = e.Value.Status.ToString(),
                description = e.Value.Description,
                duration = e.Value.Duration.TotalMilliseconds
            })
        });
    }
});

Health Checks

Basic Health Check

public class DatabaseHealthCheck : IHealthCheck
{
    private readonly AppDbContext _context;
    
    public DatabaseHealthCheck(AppDbContext context)
    {
        _context = context;
    }
    
    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        try
        {
            await _context.Database.CanConnectAsync(cancellationToken);
            return HealthCheckResult.Healthy();
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy(
                "Database connection failed", 
                ex);
        }
    }
}

Đăng ký Health Check

builder.Services.AddHealthChecks()
    .AddCheck<DatabaseHealthCheck>("database")
    .AddCheck<ExternalApiHealthCheck>("external_api")
    .AddUrlGroup(new Uri("https://api.example.com/health"), "api");

Bất đồng bộ

IAsyncEnumerable

Giới thiệu

IAsyncEnumerable<T> cho phép stream dữ liệu bất đồng bộ, tốt cho việc xử lý large datasets.

Sử dụng

public async IAsyncEnumerable<Product> GetProductsStreamAsync()
{
    using var connection = new SqlConnection(connectionString);
    await connection.OpenAsync();
    
    using var command = new SqlCommand("SELECT * FROM Products", connection);
    using var reader = await command.ExecuteReaderAsync();
    
    while (await reader.ReadAsync())
    {
        yield return new Product
        {
            Id = reader.GetInt32(0),
            Name = reader.GetString(1),
            Price = reader.GetDecimal(2)
        };
    }
}

// Sử dụng
await foreach (var product in GetProductsStreamAsync())
{
    Console.WriteLine(product.Name);
}

So sánh với IEnumerable

// IEnumerable - Block thread, load all to memory
public IEnumerable<Product> GetProductsSync()
{
    var products = _context.Products.ToList(); // Load all
    foreach (var p in products)
        yield return p;
}

// IAsyncEnumerable - Non-blocking, stream data
public async IAsyncEnumerable<Product> GetProductsAsync()
{
    await foreach (var p in _context.Products.AsAsyncEnumerable())
        yield return p;
}

Kỹ thuật Stream dữ liệu lớn

Chunking

public async Task ProcessLargeDatasetAsync()
{
    const int batchSize = 1000;
    var skip = 0;
    
    while (true)
    {
        var batch = await _context.Products
            .AsNoTracking()
            .OrderBy(p => p.Id)
            .Skip(skip)
            .Take(batchSize)
            .ToListAsync();
        
        if (batch.Count == 0)
            break;
        
        // Process batch
        await ProcessBatchAsync(batch);
        
        skip += batchSize;
        Console.WriteLine($"Processed {skip} items");
    }
}

Parallel Processing

public async Task ProcessInParallelAsync()
{
    var products = await _context.Products
        .AsNoTracking()
        .Where(p => !p.Processed)
        .ToListAsync();
    
    var options = new ParallelOptions { MaxDegreeOfParallelism = 4 };
    
    await Parallel.ForEachAsync(products, options, async (product, ct) =>
    {
        await ProcessProductAsync(product);
    });
}

Cancellation Token

public async Task<List<Product>> GetProductsAsync(
    CancellationToken cancellationToken = default)
{
    var result = new List<Product>();
    
    await foreach (var product in GetProductsStreamAsync())
    {
        cancellationToken.ThrowIfCancellationRequested();
        
        result.Add(product);
        
        if (result.Count >= 1000)
            break;
    }
    
    return result;
}

// Sử dụng với timeout
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));

try
{
    var products = await GetProductsAsync(cts.Token);
}
catch (OperationCanceledException)
{
    Console.WriteLine("Operation timed out");
}

Channel (Producer-Consumer)

// Producer
public class DataProducer
{
    private readonly Channel<int> _channel;
    
    public DataProducer()
    {
        _channel = Channel.CreateBounded<int>(100);
    }
    
    public async Task ProduceAsync(CancellationToken ct)
    {
        for (var i = 0; i < 10000; i++)
        {
            await _channel.Writer.WriteAsync(i, ct);
        }
        _channel.Writer.Complete();
    }
    
    public ChannelReader<int> Reader => _channel.Reader;
}

// Consumer
public class DataConsumer
{
    private readonly ChannelReader<int> _reader;
    
    public DataConsumer(ChannelReader<int> reader)
    {
        _reader = reader;
    }
    
    public async Task ConsumeAsync(CancellationToken ct)
    {
        await foreach (var item in _reader.ReadAllAsync(ct))
        {
            await ProcessAsync(item);
        }
    }
}

ValueTask vs Task

// Task - Always allocate
public async Task<int> GetValueAsync()
{
    await Task.Delay(1);
    return 42;
}

// ValueTask - Avoid allocation for synchronous completion
public async ValueTask<int> GetValueAsync()
{
    if (_cache.TryGetValue(out int value))
        return value; // Synchronous - no allocation
    
    return new ValueTask<int>(42); // Async path
}
AspectTaskValueTask
AllocationAlways heapAvoided if sync
Use caseStandard asyncHot path
Synchronous returnNot allowedAllowed

7. Hệ thống Phân tán

Giới thiệu

Phần này trình bày các kiến thức về hệ thống phân tán, message queue, và container orchestration.

Nội dung chính

Message Queue

Container & Cloud

Message Queue

Overview

Message Queue là một architectural pattern cho phép các components giao tiếp với nhau một cách asynchronous thông qua messages. Trong hệ thống phân tán, message queues đóng vai trò thiết yếu trong việc decouple producers và consumers, đảm bảo reliable delivery và enable scaling.

┌─────────────────────────────────────────────────────────────────┐
│                    MESSAGE QUEUE ARCHITECTURE                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   Producer              Queue              Consumer             │
│       │                  │                    │                 │
│       │   ┌──────────┐   │                    │                 │
│       ├──▶│ Message  │──▶│                    │                 │
│       │   └──────────┘   │   ┌──────────┐    │                 │
│       │                  ├──▶│ Message  │───▶│                 │
│       │                  │   └──────────┘    │                 │
│       │                  │                    │                 │
│       │                  │   ┌──────────┐    │                 │
│       └──▶  (async) ────▶│──▶│ Message  │────│──▶ Process      │
│                          │   └──────────┘    │                 │
│                          │                    │                 │
│   Non-blocking           │                    │                 │
│   Reliable delivery       │                    │                 │
└─────────────────────────────────────────────────────────────────┘

Table of Contents

1. Introduction

  • Introduction - Core concepts, benefits, and types of message queues

2. Azure Queue Storage

3. Best Practices

4. Multiple Queues

5. Azure Service Bus

Quick Comparison

ServiceTypeMax MessageBest For
Azure Queue StoragePoint-to-Point64 KBSimple, cost-effective
Azure Service BusPub/Sub256 KBEnterprise features
RabbitMQHybrid64 MBCustom deployment
KafkaLog-basedUnlimitedHigh throughput

Getting Started

Choose the Right Service

// Simple point-to-point - Use Azure Queue Storage
var queueClient = new QueueClient(connectionString, "orders-queue");
await queueClient.SendMessageAsync(message);

// Pub/Sub needed - Use Azure Service Bus
var topicClient = new TopicClient(connectionString, "orders-topic");
await topicClient.SendMessageAsync(message);

Common Use Cases

Use CaseRecommended Service
Order ProcessingAzure Queue
Background JobsAzure Queue
Email NotificationsService Bus Topics
Event DistributionService Bus Topics
MicroservicesService Bus

Key Concepts

1. Producer

Component tạo và gửi messages vào queue.

// Fire and forget - non-blocking
await _queueClient.SendMessageAsync(new QueueMessage
{
    Body = BinaryData.FromObjectAsJson(order)
});

2. Queue

Buffer lưu trữ messages cho đến khi được xử lý.

3. Consumer

Component nhận và xử lý messages từ queue.

var messages = await _queueClient.ReceiveMessagesAsync(maxMessages: 10);
foreach (var message in messages.Value)
{
    await ProcessMessageAsync(message);
    await _queueClient.DeleteMessageAsync(message.MessageId, message.PopReceipt);
}

Next Steps

References

Message Queue - Introduction

Overview

Message Queue là một architectural pattern cho phép các components giao tiếp với nhau một cách asynchronous thông qua messages. Trong hệ thống phân tán, message queues đóng vai trò thiết yếu trong việc decouple producers và consumers, đảm bảo reliable delivery và enable scaling.

┌─────────────────────────────────────────────────────────────────┐
│                    MESSAGE QUEUE ARCHITECTURE                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   Producer              Queue              Consumer             │
│       │                  │                    │                 │
│       │   ┌──────────┐   │                    │                 │
│       ├──▶│ Message  │──▶│                    │                 │
│       │   └──────────┘   │   ┌──────────┐    │                 │
│       │                  ├──▶│ Message  │───▶│                 │
│       │                  │   └──────────┘    │                 │
│       │                  │                    │                 │
│       │                  │   ┌──────────┐    │                 │
│       └──▶  (async) ────▶│──▶│ Message  │────│──▶ Process      │
│                          │   └──────────┘    │                 │
│                          │                    │                 │
│   Non-blocking           │                    │                 │
│   Reliable delivery       │                    │                 │
└─────────────────────────────────────────────────────────────────┘

Core Concepts

1. Producer

Component tạo và gửi messages vào queue.

// Producer sends message without waiting for processing
public class OrderService
{
    private readonly QueueClient _queueClient;
    
    public async Task CreateOrder(Order order)
    {
        var message = new QueueMessage
        {
            Body = BinaryData.FromObjectAsJson(order),
            MessageId = Guid.NewGuid().ToString()
        };
        
        // Fire and forget - non-blocking
        await _queueClient.SendMessageAsync(message);
        
        // Continue with other work immediately
        return order.Id;
    }
}

2. Queue

Buffer lưu trữ messages cho đến khi được xử lý.

┌─────────────────────────────────────────────────────────────────┐
│                         QUEUE STRUCTURE                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   ┌─────────────────────────────────────────────────────────┐  │
│   │  Message 1  │  Message 2  │  Message 3  │  Message N    │  │
│   │  (FIFO)     │  (FIFO)     │  (FIFO)     │  (FIFO)       │  │
│   └─────────────────────────────────────────────────────────┘  │
│                                                                  │
│   Properties:                                                   │
│   - Name: Unique identifier                                     │
│   - Size: Current message count                                 │
│   - TTL: Time-to-live for messages                              │
│   - VisibilityTimeout: Time message is invisible              │
└─────────────────────────────────────────────────────────────────┘

3. Consumer

Component nhận và xử lý messages từ queue.

// Consumer processes messages
public class OrderProcessor : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var messages = await _queueClient.ReceiveMessagesAsync(
                maxMessages: 10,
                visibilityTimeout: TimeSpan.FromMinutes(5));
            
            foreach (var message in messages.Value)
            {
                await ProcessMessageAsync(message);
                await _queueClient.DeleteMessageAsync(
                    message.MessageId, 
                    message.PopReceipt);
            }
        }
    }
}

Message Queue Benefits

BenefitDescription
DecouplingProducers và consumers không cần biết về nhau
ReliabilityMessages được lưu persistently cho đến khi processed
ScalabilityThêm consumers không ảnh hưởng producers
Load LevelingXử lý traffic spikes bằng cách queue messages
AsynchronousNon-blocking communication

Types of Message Queues

1. Point-to-Point

Một message chỉ được xử lý bởi một consumer.

┌─────────────────────────────────────────────────────────────────┐
│                   POINT-TO-POINT PATTERN                       │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   Producer                                                      │
│       │                                                         │
│       ├──▶ [Queue] ──▶ Consumer A                             │
│       │                                                         │
│       └──▶ [Queue] ──▶ Consumer B  (không nhận được)          │
│                                                                  │
│   Message chỉ được xử lý một lần                               │
└─────────────────────────────────────────────────────────────────┘

2. Pub/Sub (Publish-Subscribe)

Một message được gửi đến tất cả subscribers.

┌─────────────────────────────────────────────────────────────────┐
│                      PUB/SUB PATTERN                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   Publisher                                                     │
│       │                                                         │
│       ├──▶ [Topic] ──▶ Subscriber 1                            │
│       │                                                        │
│       ├──▶ [Topic] ──▶ Subscriber 2                            │
│       │                                                        │
│       └──▶ [Topic] ──▶ Subscriber 3                            │
│                                                                  │
│   Tất cả subscribers nhận được cùng message                    │
└─────────────────────────────────────────────────────────────────┘

When to Use Message Queue

Use CaseDescription
Order ProcessingXử lý đơn hàng asynchronously
Background JobsScheduled tasks, batch processing
Microservices CommunicationService-to-service messaging
Email/NotificationsAsync notification delivery
File ProcessingUpload, transform, process
Event-Driven ArchitectureSystem-wide event distribution

Common Message Queue Services

ServiceProviderTypeBest For
Azure Queue StorageMicrosoftPoint-to-PointSimple, cost-effective
Azure Service BusMicrosoftPub/SubEnterprise features
RabbitMQOpen SourceHybridFlexible deployment
KafkaOpen SourceLog-basedHigh throughput
AWS SQSAmazonPoint-to-PointAWS integration
AWS SNSAmazonPub/SubAWS integration

Next Steps

References

Azure Queue Storage

Overview

Azure Queue Storage là một dịch vụ message queue được quản lý hoàn toàn bởi Microsoft Azure, cung cấp reliable, persistent message storage với chi phí thấp.

┌─────────────────────────────────────────────────────────────────┐
│                  AZURE QUEUE ARCHITECTURE                       │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌─────────────┐              ┌─────────────┐                  │
│  │   Web App   │              │   Backend   │                  │
│  │  (Producer) │────────────▶│   Worker    │                  │
│  └─────────────┘              │  (Consumer) │                  │
│        │                      └─────────────┘                  │
│        │                             │                          │
│        │    ┌───────────────────────┼───────────────────────┐ │
│        │    │         AZURE STORAGE ACCOUNT                 │ │
│        │    │  ┌────────────┐  ┌────────────┐  ┌──────────┐  │ │
│        │    │  │    QUEUE   │  │    BLOB    │  │   TABLE │  │ │
│        │    │  │ (Messages) │  │  (Files)  │  │  (Data) │  │ │
│        │    │  └────────────┘  └────────────┘  └──────────┘  │ │
│        │    └───────────────────────────────────────────────┘ │
│        │                                                       │
│        ▼                                                       │
│   Azure SDK                                                     │
└─────────────────────────────────────────────────────────────────┘

Setup

Installation

dotnet add package Azure.Storage.Queues

Configuration

// Connection string from Azure Portal
var connectionString = "DefaultEndpointsProtocol=https;AccountName=mystorageaccount;AccountKey=your-key==;EndpointSuffix=core.windows.net";

// Create client
var queueClient = new QueueClient(connectionString, "orders-queue");

// Create queue if not exists
await queueClient.CreateIfNotExistsAsync();

Dependency Injection Setup

public class Program
{
    public static void Main(string[] args)
    {
        var builder = Host.CreateApplicationBuilder(args);
        
        // Register QueueClient
        builder.Services.AddSingleton(sp =>
        {
            var connectionString = builder.Configuration.GetValue<string>("AzureStorage");
            var client = new QueueClient(connectionString, "orders-queue");
            client.CreateIfNotExists();
            return client;
        });
        
        // Or use IOptions pattern for configuration
        builder.Services.Configure<QueueOptions>(
            builder.Configuration.GetSection("Queue"));
        
        var host = builder.Build();
        host.Run();
    }
}

public class QueueOptions
{
    public string ConnectionString { get; set; }
    public string OrdersQueue { get; set; } = "orders-queue";
    public string NotificationsQueue { get; set; } = "notifications-queue";
}

Sending Messages

Basic Send

public class OrderQueueService
{
    private readonly QueueClient _queueClient;
    
    public OrderQueueService(QueueClient queueClient)
    {
        _queueClient = queueClient;
    }
    
    public async Task SendOrderAsync(Order order)
    {
        var message = new QueueMessage
        {
            Body = BinaryData.FromObjectAsJson(order),
            MessageId = Guid.NewGuid().ToString(),
            VisibilityTimeout = TimeSpan.Zero,
            TimeToLive = TimeSpan.FromDays(7)
        };
        
        await _queueClient.SendMessageAsync(message);
        
        Console.WriteLine($"Order {order.Id} sent to queue");
    }
}

// Order model
public class Order
{
    public Guid Id { get; set; }
    public string CustomerEmail { get; set; }
    public List<OrderItem> Items { get; set; }
    public decimal TotalAmount { get; set; }
    public DateTime CreatedAt { get; set; }
}

public class OrderItem
{
    public string ProductName { get; set; }
    public int Quantity { get; set; }
    public decimal Price { get; set; }
}

Send with Metadata

public async Task SendOrderWithMetadataAsync(Order order)
{
    var message = new QueueMessage
    {
        Body = BinaryData.FromObjectAsJson(order),
        TimeToLive = TimeSpan.FromDays(7),
        Metadata = new Dictionary<string, string>
        {
            ["OrderId"] = order.Id.ToString(),
            ["CustomerEmail"] = order.CustomerEmail,
            ["Priority"] = order.TotalAmount > 1000 ? "High" : "Normal"
        }
    };
    
    await _queueClient.SendMessageAsync(message);
}

Batch Send

public async Task SendOrdersBatchAsync(IEnumerable<Order> orders)
{
    var messages = orders.Select(order => new QueueMessage
    {
        Body = BinaryData.FromObjectAsJson(order),
        MessageId = Guid.NewGuid().ToString()
    });
    
    // Azure Queue supports batch sending
    foreach (var message in messages)
    {
        await _queueClient.SendMessageAsync(message);
    }
}

Receiving Messages

Basic Receive

public class OrderProcessor : BackgroundService
{
    private readonly QueueClient _queueClient;
    private readonly ILogger<OrderProcessor> _logger;
    
    public OrderProcessor(QueueClient queueClient, ILogger<OrderProcessor> logger)
    {
        _queueClient = queueClient;
        _logger = logger;
    }
    
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                // Receive up to 32 messages
                var messages = await _queueClient.ReceiveMessagesAsync(
                    maxMessages: 32,
                    visibilityTimeout: TimeSpan.FromMinutes(5),
                    cancellationToken: stoppingToken);
                
                foreach (var message in messages.Value)
                {
                    try
                    {
                        var order = JsonSerializer.Deserialize<Order>(message.Body.ToString());
                        
                        _logger.LogInformation("Processing order: {OrderId}", order.Id);
                        
                        await ProcessOrderAsync(order);
                        
                        // Delete message after successful processing
                        await _queueClient.DeleteMessageAsync(
                            message.MessageId,
                            message.PopReceipt,
                            stoppingToken);
                        
                        _logger.LogInformation("Order processed: {OrderId}", order.Id);
                    }
                    catch (Exception ex)
                    {
                        _logger.LogError(ex, "Error processing message: {MessageId}", 
                            message.MessageId);
                    }
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error receiving messages");
                await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
            }
        }
    }
    
    private async Task ProcessOrderAsync(Order order)
    {
        await Task.Delay(100); // Simulate processing
    }
}

Peek Messages

// Peek messages without removing them from queue
public async Task PeekMessagesAsync()
{
    var peekedMessages = await _queueClient.PeekMessagesAsync(maxMessages: 10);
    
    foreach (var message in peekedMessages.Value)
    {
        Console.WriteLine($"Message ID: {message.MessageId}");
        Console.WriteLine($"Body: {message.Body}");
    }
}

Update Message Visibility

// Extend visibility timeout if processing takes longer
public async Task ExtendProcessingTimeAsync(QueueMessage message)
{
    await _queueClient.UpdateMessageAsync(
        message.MessageId,
        message.PopReceipt,
        visibilityTimeout: TimeSpan.FromMinutes(10));
}

Queue Properties

// Get queue properties
public async Task GetQueueInfoAsync()
{
    var properties = await _queueClient.GetPropertiesAsync();
    
    Console.WriteLine($"Approximate Message Count: {properties.Value.ApproximateMessageCount}");
    Console.WriteLine($"Created On: {properties.Value.CreatedOn}");
    Console.WriteLine($"Last Modified: {properties.Value.LastModified}");
}

Best Practices

1. Always Use CancellationToken

public async Task SendWithCancellationAsync(Order order, CancellationToken ct)
{
    var message = new QueueMessage
    {
        Body = BinaryData.FromObjectAsJson(order)
    };
    
    await _queueClient.SendMessageAsync(message, cancellationToken: ct);
}

2. Handle Message Overflow

// Messages larger than 64KB should use Azure Blob
public async Task SendLargeOrderAsync(Order order)
{
    // Store large data in Blob
    var blobClient = new BlobClient(
        "DefaultEndpointsProtocol=https;AccountName=...;AccountKey==",
        "order-payloads",
        $"{order.Id}.json");
    
    await blobClient.UploadAsync(BinaryData.FromObjectAsJson(order));
    
    // Send reference in queue
    var message = new QueueMessage
    {
        Body = BinaryData.FromObjectAsJson(new OrderReference
        {
            OrderId = order.Id,
            PayloadUri = blobClient.Uri.ToString()
        })
    };
    
    await _queueClient.SendMessageAsync(message);
}

3. Configure Appropriate TTL

// Set appropriate TTL based on message urgency
var urgentMessage = new QueueMessage
{
    Body = BinaryData.FromObjectAsJson(order),
    TimeToLive = TimeSpan.FromMinutes(30)  // Short TTL for urgent
};

var normalMessage = new QueueMessage
{
    Body = BinaryData.FromObjectAsJson(order),
    TimeToLive = TimeSpan.FromDays(7)  // Long TTL for normal
};

Summary

FeatureLimit
Max message size64 KB
Max queue size500 TB
Max messages per retrieval32
Default visibility timeout30 seconds
Max visibility timeout7 days
Max TTL7 days

Next Steps

References

Queue Best Practices

Overview

Khi làm việc với message queues, việc xử lý errors, retries, và dead letters là rất quan trọng để đảm bảo reliability của hệ thống. Phần này trình bày các best practices để handle failures một cách graceful.

Retry Pattern with Exponential Backoff

public class ReliableQueueProcessor
{
    private readonly QueueClient _queueClient;
    private readonly ILogger<ReliableQueueProcessor> _logger;
    private readonly int MaxRetries = 3;
    
    public ReliableQueueProcessor(
        QueueClient queueClient, 
        ILogger<ReliableQueueProcessor> logger)
    {
        _queueClient = queueClient;
        _logger = logger;
    }
    
    public async Task ProcessWithRetryAsync()
    {
        var messages = await _queueClient.ReceiveMessagesAsync(
            maxMessages: 10,
            visibilityTimeout: TimeSpan.FromMinutes(5));
        
        foreach (var message in messages.Value)
        {
            var attempt = 0;
            var success = false;
            
            while (attempt < MaxRetries && !success)
            {
                try
                {
                    await ProcessMessageAsync(message);
                    
                    // Success - delete message
                    await _queueClient.DeleteMessageAsync(
                        message.MessageId, 
                        message.PopReceipt);
                    
                    success = true;
                }
                catch (Exception ex)
                {
                    attempt++;
                    _logger.LogWarning(
                        ex, 
                        "Attempt {Attempt} failed for message {MessageId}. Retrying...",
                        attempt,
                        message.MessageId);
                    
                    if (attempt < MaxRetries)
                    {
                        // Exponential backoff
                        var delay = TimeSpan.FromSeconds(Math.Pow(2, attempt));
                        await Task.Delay(delay);
                    }
                    else
                    {
                        // All retries failed - send to dead letter
                        await SendToDeadLetterAsync(message, ex);
                    }
                }
            }
        }
    }
    
    private async Task ProcessMessageAsync(QueueMessage message)
    {
        var order = JsonSerializer.Deserialize<Order>(message.Body.ToString());
        
        // Processing logic
        await Task.Delay(100);
        
        // Simulate potential failure
        if (order.TotalAmount > 10000)
            throw new Exception("High value order requires manual approval");
    }
}

Dead Letter Queue

Setting Up Dead Letter Queue

// Create dead letter queue
public async Task SetupDeadLetterQueueAsync()
{
    var connectionString = "DefaultEndpointsProtocol=https;...";
    var queueName = "orders-queue";
    
    var queueClient = new QueueClient(connectionString, queueName);
    await queueClient.CreateIfNotExistsAsync();
    
    // Create dead letter queue
    var deadLetterQueueName = $"{queueName}-deadletter";
    var deadLetterClient = new QueueClient(connectionString, deadLetterQueueName);
    await deadLetterClient.CreateIfNotExistsAsync();
}

Sending to Dead Letter

public class DeadLetterHandler
{
    private readonly QueueClient _queueClient;
    
    public async Task SendToDeadLetterAsync(QueueMessage message, Exception ex)
    {
        var deadLetterQueueName = $"{_queueClient.Name}-deadletter";
        var deadLetterClient = new QueueClient(_queueClient.ConnectionString, deadLetterQueueName);
        
        await deadLetterClient.CreateIfNotExistsAsync();
        
        var deadLetterMessage = new QueueMessage
        {
            Body = message.Body,
            MessageId = message.MessageId,
            TimeToLive = TimeSpan.FromDays(30),
            // Add metadata about the failure
            Metadata = new Dictionary<string, string>
            {
                ["OriginalQueue"] = _queueClient.Name,
                ["FailedAt"] = DateTime.UtcNow.ToString("O"),
                ["ErrorMessage"] = ex.Message,
                ["RetryCount"] = "3"
            }
        };
        
        await deadLetterClient.SendMessageAsync(deadLetterMessage);
        
        // Delete from original queue
        await _queueClient.DeleteMessageAsync(message.MessageId, message.PopReceipt);
    }
}

Poison Message Handling

Detecting Poison Messages

public class PoisonMessageHandler
{
    private readonly QueueClient _queueClient;
    private readonly ILogger<PoisonMessageHandler> _logger;
    private const int MaxDequeueCount = 5;
    
    public async Task HandlePoisonMessagesAsync()
    {
        var messages = await _queueClient.ReceiveMessagesAsync(maxMessages: 10);
        
        foreach (var message in messages.Value)
        {
            // Get message properties to check dequeue count
            var properties = await _queueClient.GetMessageAsync(message.MessageId);
            
            // If message has been tried too many times, it's a poison message
            if (properties.Value.DequeueCount > MaxDequeueCount)
            {
                await HandlePoisonMessageAsync(message);
            }
            else
            {
                // Normal processing
                await ProcessMessageAsync(message);
            }
        }
    }
    
    private async Task HandlePoisonMessageAsync(QueueMessage message)
    {
        _logger.LogError(
            "Poison message detected: {MessageId}, DequeueCount: {DequeueCount}",
            message.MessageId,
            message.DequeueCount);
        
        // 1. Log the message content for investigation
        _logger.LogInformation("Poison message content: {Body}", message.Body.ToString());
        
        // 2. Archive to blob storage for analysis
        await ArchiveMessageAsync(message);
        
        // 3. Delete from queue
        await _queueClient.DeleteMessageAsync(message.MessageId, message.PopReceipt);
        
        // 4. Notify operations team
        await NotifyOperationsAsync(message);
    }
    
    private async Task ArchiveMessageAsync(QueueMessage message)
    {
        var blobClient = new BlobClient(
            "DefaultEndpointsProtocol=https;AccountName=...;AccountKey==",
            "poison-messages",
            $"poison-{message.MessageId}-{DateTime.UtcNow:yyyyMMddHHmmss}.json");
        
        await blobClient.UploadAsync(new BinaryData(message.Body));
    }
    
    private async Task NotifyOperationsAsync(QueueMessage message)
    {
        _logger.LogError("ALERT: Poison message requires attention");
    }
}

Circuit Breaker Pattern

public class CircuitBreakerQueueProcessor
{
    private readonly QueueClient _queueClient;
    private readonly ILogger<CircuitBreakerQueueProcessor> _logger;
    
    private int _failureCount = 0;
    private readonly int _failureThreshold = 5;
    private readonly TimeSpan _circuitOpenDuration = TimeSpan.FromMinutes(1);
    private DateTime _circuitOpenedAt = DateTime.MinValue;
    
    public CircuitBreakerQueueProcessor(
        QueueClient queueClient, 
        ILogger<CircuitBreakerQueueProcessor> logger)
    {
        _queueClient = queueClient;
        _logger = logger;
    }
    
    public async Task ProcessMessagesAsync()
    {
        // Check if circuit is open
        if (IsCircuitOpen())
        {
            _logger.LogWarning("Circuit breaker is open. Waiting before retry...");
            return;
        }
        
        try
        {
            var messages = await _queueClient.ReceiveMessagesAsync(maxMessages: 10);
            
            foreach (var message in messages.Value)
            {
                await ProcessMessageAsync(message);
                _failureCount = 0;  // Reset on success
            }
        }
        catch (Exception ex)
        {
            _failureCount++;
            _logger.LogError(ex, 
                "Circuit breaker: Failure count {Count}/{Threshold}", 
                _failureCount, _failureThreshold);
            
            if (_failureCount >= _failureThreshold)
            {
                _circuitOpenedAt = DateTime.UtcNow;
                _logger.LogError("Circuit breaker opened due to repeated failures");
            }
            
            throw;
        }
    }
    
    private bool IsCircuitOpen()
    {
        if (_circuitOpenedAt == DateTime.MinValue)
            return false;
        
        if (DateTime.UtcNow - _circuitOpenedAt > _circuitOpenDuration)
        {
            // Try to close the circuit
            _circuitOpenedAt = DateTime.MinValue;
            _failureCount = 0;
            _logger.LogInformation("Circuit breaker closed, resuming normal operation");
            return false;
        }
        
        return true;
    }
}

Idempotency

Luôn thiết kế để xử lý idempotent - xử lý cùng message nhiều lần không gây ra side effects không mong muốn.

public class IdempotentOrderProcessor
{
    private readonly IRedisCache _redis;
    private readonly IOrderService _orderService;
    
    public async Task ProcessOrderAsync(QueueMessage message)
    {
        var order = JsonSerializer.Deserialize<Order>(message.Body.ToString());
        
        // Check if already processed using message ID or business key
        var isProcessed = await _redis.SetAddAsync(
            $"order:{order.Id}:processed",
            message.MessageId,
            TimeSpan.FromDays(1));
        
        if (!isProcessed)
        {
            // Already processed - skip
            return;
        }
        
        // Process the order
        await _orderService.ProcessAsync(order);
    }
}

// Alternative: Use database to track processed messages
public class DatabaseIdempotentProcessor
{
    private readonly AppDbContext _context;
    
    public async Task<bool> IsMessageProcessedAsync(string messageId)
    {
        return await _context.ProcessedMessages
            .AnyAsync(m => m.MessageId == messageId);
    }
    
    public async Task MarkAsProcessedAsync(string messageId)
    {
        _context.ProcessedMessages.Add(new ProcessedMessage
        {
            MessageId = messageId,
            ProcessedAt = DateTime.UtcNow
        });
        await _context.SaveChangesAsync();
    }
}

Error Handling Strategies

Strategy 1: Retry with Delay

public async Task ProcessWithRetryAsync(QueueMessage message)
{
    const int maxRetries = 3;
    
    for (int i = 0; i < maxRetries; i++)
    {
        try
        {
            await ProcessMessageAsync(message);
            return;
        }
        catch (Exception ex) when (i < maxRetries - 1)
        {
            await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, i)));
        }
    }
    
    // Final attempt failed - send to dead letter
    await SendToDeadLetterAsync(message, ex);
}

Strategy 2: Move to Backoff Queue

public async Task MoveToBackoffQueueAsync(QueueMessage message)
{
    var backoffQueue = new QueueClient(
        _queueClient.ConnectionString, 
        $"{_queueClient.Name}-backoff");
    
    await backoffQueue.CreateIfNotExistsAsync();
    
    var backoffMessage = new QueueMessage
    {
        Body = message.Body,
        MessageId = message.MessageId,
        TimeToLive = TimeSpan.FromMinutes(30),  // Short TTL for retry
        VisibilityTimeout = TimeSpan.FromMinutes(5)  // Wait before retry
    };
    
    await backoffQueue.SendMessageAsync(backoffMessage);
    await _queueClient.DeleteMessageAsync(message.MessageId, message.PopReceipt);
}

Strategy 3: Partial Processing

public async Task<bool> ProcessWithPartialSuccessAsync(QueueMessage message)
{
    var order = JsonSerializer.Deserialize<Order>(message.Body.ToString());
    
    try
    {
        // Process what we can
        await _orderService.ValidateOrderAsync(order);
        await _inventoryService.ReserveItemsAsync(order);
    }
    catch (InventoryException ex)
    {
        // Not enough inventory - handle gracefully
        await _notificationService.NotifyCustomerAsync(
            order.CustomerEmail,
            "Some items are out of stock");
        
        // Continue with available items
        order.Items = order.Items.Where(i => i.IsAvailable).ToList();
    }
    
    try
    {
        await _paymentService.ProcessPaymentAsync(order);
    }
    catch (PaymentException ex)
    {
        // Payment failed - retry later
        throw;
    }
    
    return true;
}

Monitoring and Alerting

public class QueueMetrics
{
    private readonly ILogger<QueueMetrics> _logger;
    private readonly QueueClient _queueClient;
    
    public async Task LogQueueMetricsAsync()
    {
        var properties = await _queueClient.GetPropertiesAsync();
        
        _logger.LogInformation(
            "Queue: {Queue}, Messages: {Count}, Created: {Created}",
            _queueClient.Name,
            properties.Value.ApproximateMessageCount,
            properties.Value.CreatedOn);
    }
}

// Health check for queue processing
public class QueueHealthCheck : IHealthCheck
{
    private readonly QueueClient _queueClient;
    
    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context, 
        CancellationToken cancellationToken)
    {
        try
        {
            var properties = await _queueClient.GetPropertiesAsync(cancellationToken);
            
            if (properties.Value.ApproximateMessageCount > 10000)
            {
                return HealthCheckResult.Degraded(
                    "Queue has large number of messages");
            }
            
            return HealthCheckResult.Healthy("Queue is healthy");
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy("Queue is not accessible", ex);
        }
    }
}

Summary

PracticeDescription
Exponential BackoffIncrease delay between retries
Dead Letter QueueStore failed messages for investigation
Poison Message HandlingDetect and archive repeatedly failing messages
Circuit BreakerStop processing when failures are too frequent
IdempotencyEnsure message can be processed multiple times safely
MonitoringTrack queue health and alert on issues

Next Steps

References

Multiple Queues Management

Overview

Khi ứng dụng cần làm việc với nhiều queue khác nhau (ví dụ: orders-queue, notifications-queue, inventory-queue), có một số approaches để quản lý. Phần này trình bày các patterns và best practices.

1. Multiple QueueClient Instances (Simple)

Phù hợp cho small applications với vài queues.

public class MultiQueueService
{
    private readonly QueueClient _ordersQueue;
    private readonly QueueClient _notificationsQueue;
    private readonly QueueClient _inventoryQueue;
    
    public MultiQueueService(IConfiguration configuration)
    {
        var connectionString = configuration.GetValue<string>("AzureStorage");
        
        _ordersQueue = new QueueClient(connectionString, "orders-queue");
        _notificationsQueue = new QueueClient(connectionString, "notifications-queue");
        _inventoryQueue = new QueueClient(connectionString, "inventory-queue");
        
        // Create queues if not exist
        _ordersQueue.CreateIfNotExists();
        _notificationsQueue.CreateIfNotExists();
        _inventoryQueue.CreateIfNotExists();
    }
    
    public async Task SendOrderAsync(Order order)
    {
        var message = new QueueMessage
        {
            Body = BinaryData.FromObjectAsJson(order),
            MessageId = Guid.NewGuid().ToString()
        };
        await _ordersQueue.SendMessageAsync(message);
    }
    
    public async Task SendNotificationAsync(Notification notification)
    {
        var message = new QueueMessage
        {
            Body = BinaryData.FromObjectAsJson(notification)
        };
        await _notificationsQueue.SendMessageAsync(message);
    }
    
    public async Task SendInventoryUpdateAsync(InventoryUpdate update)
    {
        var message = new QueueMessage
        {
            Body = BinaryData.FromObjectAsJson(update)
        };
        await _inventoryQueue.SendMessageAsync(message);
    }
}

Interface + Factory pattern, tốt cho medium applications.

// Interface định nghĩa factory
public interface IQueueClientFactory
{
    QueueClient GetQueueClient(string queueName);
}

// Implementation
public class QueueClientFactory : IQueueClientFactory
{
    private readonly string _connectionString;
    private readonly Dictionary<string, QueueClient> _clients = new();
    
    public QueueClientFactory(string connectionString)
    {
        _connectionString = connectionString;
    }
    
    public QueueClient GetQueueClient(string queueName)
    {
        if (!_clients.TryGetValue(queueName, out var client))
        {
            client = new QueueClient(_connectionString, queueName);
            client.CreateIfNotExists();
            _clients[queueName] = client;
        }
        
        return client;
    }
}

// Usage in services
public class OrderService
{
    private readonly QueueClient _ordersQueue;
    
    public OrderService(IQueueClientFactory queueFactory)
    {
        _ordersQueue = queueFactory.GetQueueClient("orders-queue");
    }
    
    public async Task SendOrderAsync(Order order)
    {
        await _ordersQueue.SendMessageAsync(new QueueMessage
        {
            Body = BinaryData.FromObjectAsJson(order)
        });
    }
}

public class NotificationService
{
    private readonly QueueClient _notificationsQueue;
    
    public NotificationService(IQueueClientFactory queueFactory)
    {
        _notificationsQueue = queueFactory.GetQueueClient("notifications-queue");
    }
}

// DI Registration
public class Program
{
    public static void Main(string[] args)
    {
        var builder = Host.CreateApplicationBuilder(args);
        
        var connectionString = builder.Configuration.GetValue<string>("AzureStorage");
        builder.Services.AddSingleton<IQueueClientFactory>(
            new QueueClientFactory(connectionString));
        
        builder.Services.AddScoped<IOrderService, OrderService>();
        builder.Services.AddScoped<INotificationService, NotificationService>();
        
        var host = builder.Build();
        host.Run();
    }
}

3. Generic Queue Service

Generic service cho bất kỳ queue nào, tốt cho large applications.

public interface IGenericQueueService
{
    Task SendAsync<T>(string queueName, T message);
    Task<IEnumerable<QueueMessage>> ReceiveAsync(string queueName, int maxMessages = 32);
    Task CompleteAsync(string queueName, QueueMessage message);
    Task DeadLetterAsync(string queueName, QueueMessage message, string errorMessage);
}

public class GenericQueueService : IGenericQueueService
{
    private readonly string _connectionString;
    private readonly Dictionary<string, QueueClient> _clients = new();
    
    public GenericQueueService(string connectionString)
    {
        _connectionString = connectionString;
    }
    
    private QueueClient GetOrCreateClient(string queueName)
    {
        if (!_clients.TryGetValue(queueName, out var client))
        {
            client = new QueueClient(_connectionString, queueName);
            client.CreateIfNotExists();
            _clients[queueName] = client;
        }
        return client;
    }
    
    public async Task SendAsync<T>(string queueName, T message)
    {
        var client = GetOrCreateClient(queueName);
        
        var queueMessage = new QueueMessage
        {
            Body = BinaryData.FromObjectAsJson(message),
            MessageId = Guid.NewGuid().ToString(),
            TimeToLive = TimeSpan.FromDays(7)
        };
        
        await client.SendMessageAsync(queueMessage);
    }
    
    public async Task<IEnumerable<QueueMessage>> ReceiveAsync(
        string queueName, 
        int maxMessages = 32)
    {
        var client = GetOrCreateClient(queueName);
        var response = await client.ReceiveMessagesAsync(maxMessages);
        return response.Value;
    }
    
    public async Task CompleteAsync(string queueName, QueueMessage message)
    {
        var client = GetOrCreateClient(queueName);
        await client.DeleteMessageAsync(message.MessageId, message.PopReceipt);
    }
    
    public async Task DeadLetterAsync(
        string queueName, 
        QueueMessage message, 
        string errorMessage)
    {
        var deadLetterQueueName = $"{queueName}-deadletter";
        var deadLetterClient = GetOrCreateClient(deadLetterQueueName);
        
        var deadLetterMessage = new QueueMessage
        {
            Body = message.Body,
            MessageId = message.MessageId,
            TimeToLive = TimeSpan.FromDays(30),
            Metadata = new Dictionary<string, string>
            {
                ["OriginalQueue"] = queueName,
                ["FailedAt"] = DateTime.UtcNow.ToString("O"),
                ["ErrorMessage"] = errorMessage
            }
        };
        
        await deadLetterClient.SendMessageAsync(deadLetterMessage);
        await CompleteAsync(queueName, message);
    }
}

// Usage
public class OrderService
{
    private readonly IGenericQueueService _queueService;
    
    public OrderService(IGenericQueueService queueService)
    {
        _queueService = queueService;
    }
    
    public async Task SendOrderAsync(Order order)
    {
        await _queueService.SendAsync("orders-queue", order);
    }
}

4. Background Service for Multiple Queues

Xử lý nhiều queues trong một background service.

public class MultiQueueProcessor : BackgroundService
{
    private readonly IConfiguration _configuration;
    private readonly ILogger<MultiQueueProcessor> _logger;
    private readonly Dictionary<string, Func<QueueMessage, Task>> _handlers;
    
    public MultiQueueProcessor(
        IConfiguration configuration,
        ILogger<MultiQueueProcessor> logger)
    {
        _configuration = configuration;
        _logger = logger;
        
        // Define handlers for each queue
        _handlers = new Dictionary<string, Func<QueueMessage, Task>>
        {
            ["orders-queue"] = ProcessOrderMessageAsync,
            ["notifications-queue"] = ProcessNotificationMessageAsync,
            ["inventory-queue"] = ProcessInventoryMessageAsync
        };
    }
    
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var connectionString = _configuration.GetValue<string>("AzureStorage");
        
        // Create tasks for each queue
        var tasks = _handlers.Select(async kvp =>
        {
            var queueName = kvp.Key;
            var handler = kvp.Value;
            
            var client = new QueueClient(connectionString, queueName);
            
            while (!stoppingToken.IsCancellationRequested)
            {
                try
                {
                    var messages = await client.ReceiveMessagesAsync(
                        maxMessages: 10,
                        visibilityTimeout: TimeSpan.FromMinutes(5),
                        cancellationToken: stoppingToken);
                    
                    foreach (var message in messages.Value)
                    {
                        try
                        {
                            await handler(message);
                            await client.DeleteMessageAsync(
                                message.MessageId, 
                                message.PopReceipt);
                        }
                        catch (Exception ex)
                        {
                            _logger.LogError(ex, 
                                "Error processing message from {Queue}", 
                                queueName);
                        }
                    }
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Error receiving from queue {Queue}", queueName);
                    await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
                }
            }
        });
        
        // Run all queue processors in parallel
        await Task.WhenAll(tasks);
    }
    
    private async Task ProcessOrderMessageAsync(QueueMessage message)
    {
        var order = JsonSerializer.Deserialize<Order>(message.Body.ToString());
        _logger.LogInformation("Processing order: {OrderId}", order.Id);
        await Task.Delay(100);
    }
    
    private async Task ProcessNotificationMessageAsync(QueueMessage message)
    {
        var notification = JsonSerializer.Deserialize<Notification>(message.Body.ToString());
        _logger.LogInformation("Sending notification: {Type}", notification.Type);
        await Task.Delay(50);
    }
    
    private async Task ProcessInventoryMessageAsync(QueueMessage message)
    {
        var inventoryUpdate = JsonSerializer.Deserialize<InventoryUpdate>(message.Body.ToString());
        _logger.LogInformation("Updating inventory: {ProductId}", inventoryUpdate.ProductId);
        await Task.Delay(50);
    }
}

5. Configuration-Based Queue Registration

Đăng ký queues từ configuration.

// appsettings.json
{
  "AzureStorage": "DefaultEndpointsProtocol=https;...",
  "Queues": {
    "Orders": "orders-queue",
    "Notifications": "notifications-queue",
    "Inventory": "inventory-queue"
  }
}

// Queue configuration class
public class QueueConfiguration
{
    public string Orders { get; set; }
    public string Notifications { get; set; }
    public string Inventory { get; set; }
}

// Registration
public class Program
{
    public static void Main(string[] args)
    {
        var builder = Host.CreateApplicationBuilder(args);
        
        var queueConfig = builder.Configuration.GetSection("Queues")
            .Get<QueueConfiguration>();
        
        // Register all queues as dictionary
        builder.Services.AddSingleton(sp =>
        {
            var connectionString = builder.Configuration.GetValue<string>("AzureStorage");
            return new Dictionary<string, QueueClient>
            {
                [queueConfig.Orders] = new QueueClient(connectionString, queueConfig.Orders),
                [queueConfig.Notifications] = new QueueClient(connectionString, queueConfig.Notifications),
                [queueConfig.Inventory] = new QueueClient(connectionString, queueConfig.Inventory)
            };
        });
        
        // Or register by name
        builder.Services.AddScoped<IOrderService>(sp =>
        {
            var queues = sp.GetRequiredService<Dictionary<string, QueueClient>>();
            return new OrderService(queues[queueConfig.Orders]);
        });
    }
}

// Usage with IOptions
public class QueueService
{
    private readonly QueueClient _ordersQueue;
    
    public QueueService(IOptions<QueueConfiguration> config, IConfiguration configuration)
    {
        var connectionString = configuration.GetValue<string>("AzureStorage");
        _ordersQueue = new QueueClient(connectionString, config.Value.Orders);
    }
}

6. Queue-Specific Processors

Tạo processor riêng cho từng queue.

// Base processor interface
public interface IQueueProcessor
{
    string QueueName { get; }
    Task ProcessMessageAsync(QueueMessage message);
}

// Order processor
public class OrderQueueProcessor : IQueueProcessor
{
    private readonly QueueClient _queueClient;
    private readonly IOrderService _orderService;
    
    public string QueueName => "orders-queue";
    
    public OrderQueueProcessor(
        QueueClient queueClient, 
        IOrderService orderService)
    {
        _queueClient = queueClient;
        _orderService = orderService;
    }
    
    public async Task ProcessMessageAsync(QueueMessage message)
    {
        var order = JsonSerializer.Deserialize<Order>(message.Body.ToString());
        await _orderService.ProcessAsync(order);
    }
}

// Notification processor
public class NotificationQueueProcessor : IQueueProcessor
{
    public string QueueName => "notifications-queue";
    
    public async Task ProcessMessageAsync(QueueMessage message)
    {
        var notification = JsonSerializer.Deserialize<Notification>(message.Body.ToString());
        // Send notification
    }
}

// Hosted service that runs all processors
public class QueueProcessorHost : BackgroundService
{
    private readonly IEnumerable<IQueueProcessor> _processors;
    private readonly ILogger<QueueProcessorHost> _logger;
    private readonly Dictionary<string, IQueueProcessor> _processorMap;
    
    public QueueProcessorHost(
        IEnumerable<IQueueProcessor> processors,
        ILogger<QueueProcessorHost> logger)
    {
        _processors = processors;
        _logger = logger;
        _processorMap = processors.ToDictionary(p => p.QueueName);
    }
    
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var tasks = _processors.Select(p => ProcessQueueAsync(p, stoppingToken));
        await Task.WhenAll(tasks);
    }
    
    private async Task ProcessQueueAsync(
        IQueueProcessor processor, 
        CancellationToken ct)
    {
        var connectionString = Environment.GetEnvironmentVariable("AzureStorage");
        var client = new QueueClient(connectionString, processor.QueueName);
        
        while (!ct.IsCancellationRequested)
        {
            try
            {
                var messages = await client.ReceiveMessagesAsync(
                    maxMessages: 10,
                    visibilityTimeout: TimeSpan.FromMinutes(5),
                    cancellationToken: ct);
                
                foreach (var message in messages.Value)
                {
                    try
                    {
                        await processor.ProcessMessageAsync(message);
                        await client.DeleteMessageAsync(
                            message.MessageId, 
                            message.PopReceipt);
                    }
                    catch (Exception ex)
                    {
                        _logger.LogError(ex, "Error processing {Queue}", processor.QueueName);
                    }
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error receiving from {Queue}", processor.QueueName);
                await Task.Delay(TimeSpan.FromSeconds(5), ct);
            }
        }
    }
}

Summary Comparison

ApproachBest ForProsCons
Multiple instancesSmall appsSimple, directRepetitive code
FactoryMedium appsDI-friendly, reusableMore setup
Generic serviceLarge appsCentralized, flexibleSlightly complex
Background serviceProcessingHandles multiple queuesSingle process
Config-basedConfig-drivenEasy to changeLess type-safe
Queue-specific processorsComplex appsSeparation of concernsMore interfaces

Next Steps

References

Azure Service Bus Topics

Overview

Azure Service Bus Topics cung cấp Pub/Sub (publish-subscribe) messaging pattern, cho phép một message được gửi đến nhiều subscribers. Đây là lựa chọn tốt hơn Azure Queue khi bạn cần một-to-nhiều communication.

┌─────────────────────────────────────────────────────────────────┐
│                  SERVICE BUS TOPICS ARCHITECTURE               │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   Publisher                                                     │
│       │                                                         │
│       ▼                                                         │
│   ┌───────────┐                                                 │
│   │   Topic   │  (OrdersTopic)                                  │
│   └─────┬─────┘                                                 │
│         │                                                       │
│    ┌────┼────┬────────────┐                                     │
│    │    │    │            │                                     │
│    ▼    ▼    ▼            ▼                                     │
│ ┌────┐┌────┐┌─────┐    ┌─────┐                                 │
│ │Sub1││Sub2││Sub3 │    │Sub4 │                                 │
│ │Email││SMS ││Webhook│    │Analytics│                            │
│ └────┘└────┘└─────┘    └─────┘                                 │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Setup

Installation

dotnet add package Azure.Messaging.ServiceBus

Configuration

// Connection string from Azure Portal
var connectionString = "Endpoint=sb://mynamespace.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey==";

Creating Topics and Subscriptions

public class ServiceBusSetup
{
    private readonly ServiceBusClient _client;
    
    public ServiceBusSetup(string connectionString)
    {
        _client = new ServiceBusClient(connectionString);
    }
    
    public async Task CreateTopicAndSubscriptionsAsync()
    {
        var adminClient = new ServiceBusAdministrationClient(
            "Endpoint=sb://mynamespace.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey==");
        
        // Create topic
        var topicOptions = new CreateTopicOptions("orders-topic")
        {
            MaxSizeInMegabytes = 1024,
            DefaultMessageTimeToLive = TimeSpan.FromDays(7),
            EnableBatchedOperations = true
        };
        
        await adminClient.CreateTopicAsync(topicOptions);
        
        // Create subscriptions
        await adminClient.CreateSubscriptionAsync(
            new CreateSubscriptionOptions("orders-topic", "email-notifications"));
        
        await adminClient.CreateSubscriptionAsync(
            new CreateSubscriptionOptions("orders-topic", "sms-notifications"));
        
        await adminClient.CreateSubscriptionAsync(
            new CreateSubscriptionOptions("orders-topic", "analytics"));
        
        // Add filters to subscriptions
        await adminClient.CreateRuleAsync(
            "orders-topic",
            "email-notifications",
            new CreateRuleOptions
            {
                Name = "high-priority-filter",
                Filter = new SqlRuleFilter("sys.Label = 'high-priority'")
            });
    }
}

Publishing Messages to Topic

public class OrderPublisher
{
    private readonly TopicClient _topicClient;
    
    public OrderPublisher(string connectionString)
    {
        _topicClient = new TopicClient(connectionString, "orders-topic");
    }
    
    public async Task PublishOrderAsync(Order order)
    {
        var message = new ServiceBusMessage
        {
            Body = BinaryData.FromObjectAsJson(order),
            ContentType = "application/json",
            Subject = "OrderCreated",
            CorrelationId = order.Id.ToString(),
            MessageId = Guid.NewGuid().ToString(),
            TimeToLive = TimeSpan.FromDays(7)
        };
        
        // Add custom properties
        message.Properties["CustomerEmail"] = order.CustomerEmail;
        message.Properties["OrderTotal"] = order.TotalAmount.ToString();
        message.Properties["Priority"] = order.TotalAmount > 1000 ? "high-priority" : "normal";
        
        await _topicClient.SendMessageAsync(message);
        
        Console.WriteLine($"Order {order.Id} published to topic");
    }
    
    public async Task PublishBatchAsync(IEnumerable<Order> orders)
    {
        var messages = orders.Select(order => new ServiceBusMessage
        {
            Body = BinaryData.FromObjectAsJson(order),
            MessageId = Guid.NewGuid().ToString()
        });
        
        await _topicClient.SendMessagesAsync(messages);
    }
}

Subscribing to Messages

Basic Subscription Processor

public class EmailNotificationProcessor : BackgroundService
{
    private readonly ServiceBusProcessor _processor;
    private readonly ILogger<EmailNotificationProcessor> _logger;
    
    public EmailNotificationProcessor(
        string connectionString,
        ILogger<EmailNotificationProcessor> logger)
    {
        _logger = logger;
        
        var options = new ServiceBusProcessorOptions
        {
            MaxConcurrentCalls = 10,
            AutoCompleteMessages = false
        };
        
        _processor = new ServiceBusClient(connectionString)
            .CreateProcessor("orders-topic", "email-notifications", options);
    }
    
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _processor.ProcessMessageAsync += ProcessMessageHandler;
        _processor.ProcessErrorAsync += ErrorHandler;
        
        await _processor.StartProcessingAsync(stoppingToken);
        
        while (!stoppingToken.IsCancellationRequested)
        {
            await Task.Delay(1000, stoppingToken);
        }
    }
    
    private async Task ProcessMessageHandler(ProcessMessageEventArgs args)
    {
        var order = JsonSerializer.Deserialize<Order>(
            args.Message.Body.ToString());
        
        _logger.LogInformation("Sending email for order: {OrderId}", order.Id);
        
        // Send email logic
        await _emailService.SendOrderConfirmationAsync(order);
        
        // Complete the message
        await args.CompleteMessageAsync(args.Message);
    }
    
    private Task ErrorHandler(ProcessErrorEventArgs args)
    {
        _logger.LogError(args.Exception, "Error processing message");
        return Task.CompletedTask;
    }
    
    public override async Task StopAsync(CancellationToken cancellationToken)
    {
        await _processor.StopProcessingAsync(cancellationToken);
        await base.StopAsync(cancellationToken);
    }
}

Multiple Subscription Processors

public class SmsNotificationProcessor : BackgroundService
{
    private readonly ServiceBusProcessor _processor;
    
    public SmsNotificationProcessor(string connectionString)
    {
        _processor = new ServiceBusClient(connectionString)
            .CreateProcessor("orders-topic", "sms-notifications");
    }
    
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _processor.ProcessMessageAsync += async args =>
        {
            var order = JsonSerializer.Deserialize<Order>(args.Message.Body.ToString());
            
            // Send SMS
            await _smsService.SendAsync(
                order.CustomerPhone,
                $"Order confirmed: {order.Id}");
            
            await args.CompleteMessageAsync(args.Message);
        };
        
        await _processor.StartProcessingAsync(stoppingToken);
    }
}

public class AnalyticsProcessor : BackgroundService
{
    private readonly ServiceBusProcessor _processor;
    
    public AnalyticsProcessor(string connectionString)
    {
        _processor = new ServiceBusClient(connectionString)
            .CreateProcessor("orders-topic", "analytics");
    }
    
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _processor.ProcessMessageAsync += async args =>
        {
            var order = JsonSerializer.Deserialize<Order>(args.Message.Body.ToString());
            
            // Record analytics
            await _analyticsService.RecordOrderAsync(order);
            
            await args.CompleteMessageAsync(args.Message);
        };
        
        await _processor.StartProcessingAsync(stoppingToken);
    }
}

Filters and Actions

SQL Filter

// Filter by message properties
var filter = new SqlRuleFilter("sys.Label = 'high-priority' AND order.TotalAmount > 1000");

// Composite filter
var compositeFilter = new SqlRuleFilter(
    "priority = 'high' OR (priority = 'medium' AND region = 'US')");

// Correlation filter (more efficient than SQL)
var correlationFilter = new CorrelationRuleFilter
{
    CorrelationId = order.Id.ToString(),
    Label = "high-priority"
};

Actions

// Update message properties when matching
var ruleOptions = new CreateRuleOptions
{
    Name = "add-priority-action",
    Filter = new SqlRuleFilter("sys.Label = 'urgent'"),
    Action = new SqlRuleAction("SET sys.Label = 'high-priority'; SET sys.TimeToLive = '3600'")
};

Example: Different Subscription for Priority

public async Task SetupPrioritySubscriptionsAsync()
{
    var adminClient = new ServiceBusAdministrationClient(connectionString);
    
    // Normal priority subscription - gets all messages
    await adminClient.CreateSubscriptionAsync(
        new CreateSubscriptionOptions("orders-topic", "all-orders"));
    
    // High priority subscription - gets only high value orders
    await adminClient.CreateRuleAsync(
        "orders-topic",
        "high-value-orders",
        new CreateRuleOptions
        {
            Name = "high-value-filter",
            Filter = new SqlRuleFilter("order.TotalAmount >= 1000")
        });
    
    // Low value subscription - gets only small orders
    await adminClient.CreateRuleAsync(
        "orders-topic",
        "low-value-orders",
        new CreateRuleOptions
        {
            Name = "low-value-filter",
            Filter = new SqlRuleFilter("order.TotalAmount < 100")
        });
}

Dead Letter and Retry

// Configure dead letter for subscription
public async Task ConfigureDeadLetterAsync()
{
    var adminClient = new ServiceBusAdministrationClient(connectionString);
    
    var subscriptionOptions = new CreateSubscriptionOptions(
        "orders-topic", 
        "email-notifications")
    {
        DeadLetterTopic = "orders-topic-deadletter",
        MaxDeliveryCount = 3,
        LockDuration = TimeSpan.FromMinutes(1)
    };
    
    await adminClient.CreateSubscriptionAsync(subscriptionOptions);
}

// Handle dead letter messages
public class DeadLetterProcessor : BackgroundService
{
    private readonly ServiceBusProcessor _processor;
    
    public DeadLetterProcessor(string connectionString)
    {
        _processor = new ServiceBusClient(connectionString)
            .CreateProcessor("orders-topic-deadletter", "all-messages");
    }
    
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _processor.ProcessMessageAsync += async args =>
        {
            var message = args.Message;
            
            // Log or analyze dead letter message
            Console.WriteLine($"Dead letter: {message.Body}");
            Console.WriteLine($"Error: {message.Properties}");
            
            await args.CompleteMessageAsync(args.Message);
        };
        
        await _processor.StartProcessingAsync(stoppingToken);
    }
}

Session Handling

// Enable sessions for ordered processing
public async Task SetupSessionAsync()
{
    var adminClient = new ServiceBusAdministrationClient(connectionString);
    
    var subscriptionOptions = new CreateSubscriptionOptions(
        "orders-topic",
        "ordered-processing")
    {
        RequiresSession = true  // Enable sessions
    };
    
    await adminClient.CreateSubscriptionAsync(subscriptionOptions);
}

// Process with session
public class SessionProcessor : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var options = new ServiceBusSessionProcessorOptions
        {
            MaxConcurrentSessions = 10,
            SessionIdleTimeout = TimeSpan.FromMinutes(1)
        };
        
        var processor = new ServiceBusClient(connectionString)
            .CreateSessionProcessor("orders-topic", "ordered-processing", options);
        
        processor.ProcessMessageAsync += async args =>
        {
            var sessionId = args.Message.SessionId;
            
            // All messages with same sessionId processed sequentially
            var order = JsonSerializer.Deserialize<Order>(args.Message.Body.ToString());
            
            await ProcessOrderInSequenceAsync(order);
            
            await args.CompleteMessageAsync(args.Message);
        };
        
        await processor.StartProcessingAsync(stoppingToken);
    }
}

Comparison: Queue vs Topics

FeatureAzure QueueService Bus Topics
PatternPoint-to-PointPub/Sub
Multiple ConsumersOne consumer per messageMultiple consumers
Message Size64 KB256 KB
SessionsNot supportedSupported
TransactionsNot supportedSupported
Duplicate DetectionNot supportedSupported
OrderingFIFOFIFO per subscription

When to Use Topics

ScenarioSolution
Multiple systems need same dataPub/Sub
Different processing per message typeFilters
Fan-out to microservicesMultiple subscriptions
Event-driven architectureTopics + Subscriptions

Summary

ComponentDescription
TopicChannel for publishing messages
SubscriptionRecipient that receives messages from topic
FilterRules to route messages to subscriptions
ActionModify message when filter matches

Next Steps

References

Container & Cloud

Docker

Dockerfile cho .NET

# Build Stage
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src

# Copy project files
COPY ["src/MyApp/MyApp.csproj", "MyApp/"]
RUN dotnet restore "MyApp/MyApp.csproj"

# Copy source and build
COPY src/MyApp/. MyApp/
WORKDIR "/src/MyApp"
RUN dotnet publish -c Release -o /app/publish

# Runtime Stage
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish .

# Non-root user
RUN groupadd -r appgroup && useradd -r -g appgroup appuser
RUN chown -R appuser:appgroup /app
USER appuser

EXPOSE 8080
ENV ASPNETCORE_URLS=http+:8080

ENTRYPOINT ["dotnet", "MyApp.dll"]

Docker Commands

# Build image
docker build -t myapp:latest .

# Run container
docker run -d -p 8080:8080 --name myapp myapp:latest

# Run với environment variables
docker run -d -p 8080:8080 \
  -e "ASPNETCORE_ENVIRONMENT=Production" \
  -e "ConnectionStrings__DefaultConnection=..." \
  myapp:latest

# Docker Compose
docker-compose.yml
version: '3.8'
services:
  app:
    build: .
    ports:
      - "8080:8080"
    depends_on:
      - db
      - redis
  db:
    image: mcr.microsoft.com/mssql/server
    environment:
      - ACCEPT_EULA=Y
      - SA_PASSWORD=YourStrong!Passw0rd
  redis:
    image: redis:alpine

Kubernetes

Pod

apiVersion: v1
kind: Pod
metadata:
  name: myapp-pod
  labels:
    app: myapp
spec:
  containers:
  - name: myapp
    image: myapp:latest
    ports:
    - containerPort: 8080
    resources:
      requests:
        memory: "64Mi"
        cpu: "250m"
      limits:
        memory: "128Mi"
        cpu: "500m"

Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: myapp:latest
        ports:
        - containerPort: 8080
        env:
        - name: ASPNETCORE_ENVIRONMENT
          value: "Production"
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /health/ready
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5

Service

apiVersion: v1
kind: Service
metadata:
  name: myapp-service
spec:
  selector:
    app: myapp
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080
  type: LoadBalancer

Cloud Deployment

Azure

# Azure Container Apps
az containerapp create \
  --name myapp \
  --resource-group mygroup \
  --image myregistry.azurecr.io/myapp:latest \
  --cpu 0.25 --memory 0.5Gi \
  --ingress external \
  --target-port 8080

# Azure Kubernetes Service
az aks create \
  --resource-group mygroup \
  --name mycluster \
  --node-count 3 \
  --enable-addons monitoring

AWS

# Amazon ECS
aws ecs create-cluster --cluster-name mycluster

# Amazon EKS
aws eks create-cluster \
  --name mycluster \
  --role-arn arn:aws:iam::123456789:role/EKSRole \
  --resources-vpc-config subnetIds=subnet-12345

Best Practices

12-Factor App

FactorDescription
CodebaseOne codebase tracked in version control
DependenciesExplicitly declare dependencies
ConfigStore config in environment
Backing ServicesTreat backing services as attached resources
Build/Release/RunStrictly separate build and run stages
ProcessesExecute app as one or more stateless processes
Port BindingExport HTTP as a service by port binding
ConcurrencyScale out via process model
DisposabilityFast startup and graceful shutdown
Dev/Prod ParityKeep development, staging, production similar
LogsTreat logs as event streams
Admin ProcessesRun admin/maintenance tasks as one-off processes

8. Kiểm thử

Giới thiệu

Phần này trình bày các kỹ thuật kiểm thử trong .NET.

Nội dung chính

Đơn vị & Tích hợp

Unit Test & Integration Test

Unit Test với xUnit

Cấu trúc Test

public class ProductServiceTests
{
    [Fact]
    public void GetProductById_ReturnsProduct_WhenProductExists()
    {
        // Arrange
        var productId = 1;
        var expectedProduct = new Product { Id = 1, Name = "Test" };
        
        var mockRepo = new Mock<IProductRepository>();
        mockRepo.Setup(r => r.GetByIdAsync(productId))
            .ReturnsAsync(expectedProduct);
            
        var service = new ProductService(mockRepo.Object);
        
        // Act
        var result = service.GetProductByIdAsync(productId).Result;
        
        // Assert
        Assert.NotNull(result);
        Assert.Equal(expectedProduct.Name, result.Name);
    }
    
    [Theory]
    [InlineData(0, false)]
    [InlineData(1, true)]
    [InlineData(100, true)]
    public void IsValidPrice_ReturnsExpected(int price, bool expected)
    {
        var service = new ProductService(null);
        var result = service.IsValidPrice(price);
        Assert.Equal(expected, result);
    }
}

Mocking với Moq

Basic Mocking

// Mock interface
var mockRepo = new Mock<IProductRepository>();

// Setup method
mockRepo.Setup(r => r.GetByIdAsync(It.IsAny<int>()))
    .ReturnsAsync(new Product { Id = 1, Name = "Test" });

// Setup property
mockRepo.SetupGet(r => r.Count)
    .Returns(10);

// Verify calls
mockRepo.Verify(r => r.GetByIdAsync(It.IsAny<int>()), Times.Once);

Mocking Returns

// Sequential returns
var mockRepo = new Mock<IProductRepository>();
mockRepo.SetupSequence(r => r.GetNextAsync())
    .ReturnsAsync(new Product { Id = 1 })
    .ReturnsAsync(new Product { Id = 2 })
    .ReturnsAsync((Product)null);
    
// Callback
var products = new List<Product>();
var mockRepo = new Mock<IProductRepository>();
mockRepo.Setup(r => r.AddAsync(It.IsAny<Product>()))
    .Callback<Product>(p => products.Add(p))
    .ReturnsAsync((Product p) => p);

Integration Testing

WebApplicationFactory

public class CustomWebApplicationFactory<TStartup> 
    : WebApplicationFactory<TStartup> where TStartup : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            // Remove existing DbContext
            var descriptor = services.SingleOrDefault(
                d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));
            if (descriptor != null)
                services.Remove(descriptor);
                
            // Add test DbContext
            services.AddDbContext<AppDbContext>(options =>
                options.UseInMemoryDatabase("TestDatabase"));
                
            // Build service provider
            var sp = services.BuildServiceProvider();
            
            // Seed data
            using var scope = sp.CreateScope();
            var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
            context.Products.Add(new Product { Name = "Test Product" });
            context.SaveChanges();
        });
    }
}

Test Class

public class ProductsControllerIntegrationTests 
    : IClassFixture<CustomWebApplicationFactory<Program>>
{
    private readonly HttpClient _client;
    private readonly CustomWebApplicationFactory<Program> _factory;
    
    public ProductsControllerIntegrationTests(
        CustomWebApplicationFactory<Program> factory)
    {
        _factory = factory;
        _client = factory.CreateClient();
    }
    
    [Fact]
    public async Task GetProducts_ReturnsProducts()
    {
        // Act
        var response = await _client.GetAsync("/api/products");
        
        // Assert
        response.EnsureSuccessStatusCode();
        var content = await response.Content.ReadAsStringAsync();
        var products = JsonSerializer.Deserialize<List<Product>>(content);
        
        Assert.NotNull(products);
        Assert.NotEmpty(products);
    }
}

Test Database

public class TestDatabaseFixture : IDisposable
{
    private readonly SqliteConnection _connection;
    public AppDbContext CreateContext() 
        => new(new DbContextOptionsBuilder<AppDbContext>()
            .UseSqlite(_connection).Options);
    
    public TestDatabaseFixture()
    {
        _connection = new SqliteConnection("DataSource=:memory:");
        _connection.Open();
        
        using var context = CreateContext();
        context.Database.EnsureCreated();
    }
    
    public void Dispose() => _connection.Dispose();
}

Test Patterns

Arrange-Act-Assert

[Fact]
public void CalculateTotal_ReturnsCorrectTotal()
{
    // Arrange
    var order = new Order
    {
        Items = new List<OrderItem>
        {
            new() { Price = 10.00m, Quantity = 2 },
            new() { Price = 5.00m, Quantity = 3 }
        }
    };
    
    // Act
    var total = order.CalculateTotal();
    
    // Assert
    Assert.Equal(35.00m, total);
}

AAA with Helper Methods

public class OrderServiceTests
{
    private readonly Mock<IOrderRepository> _mockRepo;
    private readonly OrderService _service;
    
    public OrderServiceTests()
    {
        _mockRepo = new Mock<IOrderRepository>();
        _service = new OrderService(_mockRepo.Object);
    }
    
    [Fact]
    public async Task CreateOrder_ReturnsOrder_WithGeneratedId()
    {
        // Arrange
        var order = CreateValidOrder();
        _mockRepo.Setup(r => r.AddAsync(It.IsAny<Order>()))
            .Callback<Order>(o => o.Id = 1)
            .ReturnsAsync((Order o) => o);
            
        // Act
        var result = await _service.CreateOrderAsync(order);
        
        // Assert
        Assert.NotEqual(Guid.Empty, result.Id);
        _mockRepo.Verify(r => r.AddAsync(It.IsAny<Order>()), Times.Once);
    }
    
    private Order CreateValidOrder() => new()
    {
        CustomerId = Guid.NewGuid(),
        Items = new List<OrderItem>
        {
            new() { ProductId = 1, Quantity = 1, Price = 10 }
        }
    };
}

Câu hỏi Phân biệt

.NET Core vs .NET Framework

Aspect.NET Core.NET Framework
PlatformCross-platform (Windows, Linux, macOS)Windows only
SourceOpen sourceMostly closed source
PerformanceBetter, optimized runtimeLegacy
ModularityPackage-based (NuGet)Full framework
CLIFull CLI supportLimited
Side-by-sideMultiple versionsSingle version per machine
Use caseModern apps, microservicesLegacy Windows apps

MVC vs Web API vs Minimal API

MVC

  • Hỗ trợ Views (Razor pages)
  • Phù hợp cho web applications với UI
  • Convention-based routing
  • Model binding đầy đủ
  • ViewBag, ViewData
public class HomeController : Controller
{
    public IActionResult Index()
    {
        ViewData["Title"] = "Home";
        return View();
    }
}

Web API

  • RESTful services, JSON/XML
  • Không có Views
  • Attribute routing
  • Content negotiation
  • [ApiController] attribute
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet]
    public IActionResult Get() => Ok();
}

Minimal APIs

  • Rút gọn code
  • Không cần Controller
  • Top-level statements
  • Phù hợp cho microservices
  • Lambda expressions
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", () => Results.Ok(new { name = "Product" }));

app.Run();

So sánh

FeatureMVCWeb APIMinimal API
Views✅ Yes❌ No❌ No
Controllers✅ Yes✅ Yes❌ No
RoutingConvention + AttributeAttributeMapGet/MapPost
Model BindingFullFullLimited
TestabilityMediumMediumHigh
Use caseWeb appsAPIsMicroservices

Abstract class vs Interface

Abstract Class

public abstract class Animal
{
    public string Name { get; set; }
    
    // Abstract method - phải implement
    public abstract void MakeSound();
    
    // Virtual method - có thể override
    public virtual void Sleep()
    {
        Console.WriteLine("Sleeping...");
    }
    
    // Non-abstract method
    public void Eat()
    {
        Console.WriteLine("Eating...");
    }
}

public class Dog : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine("Woof!");
    }
}

Interface

public interface IAnimal
{
    string Name { get; set; }
    void MakeSound();
}

public interface IFlyable
{
    void Fly();
}

public interface IPlayable
{
    void Play();
}

So sánh

AspectAbstract ClassInterface
Multiple inheritance❌ No✅ Yes
Constructor✅ Yes❌ No
Fields✅ Yes❌ No (properties only)
Access modifiers✅ Yes❌ No (public implicit)
Default implementation✅ Yes✅ Yes (C# 8+)
State✅ Can have❌ No
InheritanceSingleMultiple
Use case“is-a” relationship“can-do” capability

IEnumerable vs IQueryable

IEnumerable

public IEnumerable<Product> GetProducts()
{
    return _context.Products; // Local collection
}

foreach (var product in GetProducts())
{
    // Process
}
  • Thực thi trên client
  • Toàn bộ data được load vào memory trước khi filter
  • Phù hợp cho small datasets hoặc in-memory data

IQueryable

public IQueryable<Product> GetProducts()
{
    return _context.Products; // Query provider
}

var results = GetProducts()
    .Where(p => p.Price > 100)
    .OrderBy(p => p.Name);
    
// Query được translate sang SQL và execute trên database
  • Thực thi trên database
  • Query được build và translate sang SQL
  • Phù hợp cho large datasets và remote data sources

So sánh

AspectIEnumerableIQueryable
LocationClient-sideServer-side
ExecutionIn-memoryDatabase query
SQL Translation❌ No✅ Yes
DeferredYesYes
Use caseIn-memory collectionsDatabase queries
PerformanceSlow with large dataOptimized

Khi nào dùng?

// ✅ IEnumerable - Khi đã có data trong memory
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var evens = numbers.Where(n => n % 2 == 0);

// ✅ IQueryable - Khi query database
var products = _context.Products
    .Where(p => p.Price > 100)
    .OrderBy(p => p.Name);

// ❌ Tránh - IEnumerable từ database
var products = _context.Products.ToList()
    .Where(p => p.Price > 100); // Load all, then filter

Các câu hỏi so sánh khác

var vs dynamic

Aspectvardynamic
Type checkingCompile-timeRuntime
IntelliSense✅ Yes❌ No
PerformanceFastSlower
Use caseType known at compileLate binding

const vs readonly

Aspectconstreadonly
EvaluationCompile-timeRuntime
ValueMust be known at compileSet at runtime
StaticAlways staticCan be instance-level
Use caseCompile-time constantsRuntime constants

string vs StringBuilder

AspectstringStringBuilder
TypeImmutableMutable
MemoryNew allocation per changeDynamic buffer
Use caseSmall strings, no changesLarge strings, many concatenations

Task.WhenAll vs await in loop

// ❌ Sequential - Chậm
foreach (var url in urls)
{
    var response = await httpClient.GetAsync(url);
}

// ✅ Parallel - Nhanh hơn
var tasks = urls.Select(url => httpClient.GetAsync(url));
var responses = await Task.WhenAll(tasks);

ReactJS Roadmap

Giới thiệu

ReactJS là thư viện JavaScript mã nguồn mở do Facebook (Meta) phát triển, dùng để xây dựng giao diện người dùng (UI) theo mô hình component-based. React sử dụng Virtual DOM để tối ưu hóa quá trình cập nhật giao diện.

Mục lục

#Mảng Kiến ThứcMô tả
1JSX & RenderingJSX syntax, Virtual DOM, Conditional rendering, List rendering
2Components & PropsFunctional components, Class components, Props, Children
3Hooks Cơ bảnuseState, useEffect, useRef, useId
4Hooks Nâng caouseReducer, useMemo, useCallback, useLayoutEffect, Custom Hooks
5Context APIcreateContext, useContext, Provider pattern
6Redux & Redux ToolkitStore, Slice, Thunk, RTK Query
7React QueryData fetching, Caching, Mutations, Pagination
8React RouterRoutes, Navigation, Nested routes, Protected routes
9Forms & ValidationControlled forms, React Hook Form, Zod validation
10StylingCSS Modules, styled-components, Tailwind CSS
11PerformanceMemo, Code splitting, Lazy loading, Profiler
12TestingJest, React Testing Library, Mock API
13React PatternsHOC, Render Props, Compound Components, Portals
14Next.js Cơ bảnSSR, SSG, ISR, App Router, Server Components

Hướng dẫn sử dụng

  1. Bắt đầu từ nền tảng: JSX → Components → Hooks
  2. State Management: Context API cho app nhỏ, Redux/Redux Toolkit cho app lớn
  3. Data Fetching: React Query là tiêu chuẩn hiện đại
  4. Thực hành: Mỗi chủ đề đều có code example thực tế

Note: Tài liệu này tập trung vào React 18+ với functional components và hooks. Class components chỉ được đề cập để hiểu legacy code.

JSX & Rendering

JSX là gì?

JSX (JavaScript XML) là cú pháp mở rộng cho JavaScript, cho phép viết HTML-like code trực tiếp trong JavaScript. JSX được Babel biên dịch thành React.createElement() calls.

// JSX syntax
const element = <h1 className="title">Hello, World!</h1>;

// Biên dịch thành
const element = React.createElement(
  'h1',
  { className: 'title' },
  'Hello, World!'
);

Quy tắc JSX

// 1. Phải có một root element (hoặc Fragment)
function App() {
  return (
    <>
      <h1>Title</h1>
      <p>Content</p>
    </>
  );
}

// 2. Mọi tag phải được đóng
const img = <img src="photo.jpg" alt="photo" />;
const input = <input type="text" />;

// 3. className thay vì class
const div = <div className="container">Content</div>;

// 4. JavaScript expressions dùng {}
const name = "Alice";
const greeting = <h1>Hello, {name}!</h1>;
const result = <p>2 + 2 = {2 + 2}</p>;

// 5. Style dùng object
const styled = (
  <div style={{ color: 'red', fontSize: '16px' }}>
    Styled text
  </div>
);

Virtual DOM

React sử dụng Virtual DOM để tối ưu hóa việc cập nhật UI thực.

┌─────────────────────────────────────────────────────────┐
│                    STATE CHANGE                         │
│                        │                               │
│                        ▼                               │
│  ┌──────────────────────────────────────────────────┐  │
│  │           New Virtual DOM Tree                   │  │
│  └──────────────────────────────────────────────────┘  │
│                        │                               │
│                   DIFFING (Reconciliation)             │
│                        │                               │
│  ┌──────────────────────────────────────────────────┐  │
│  │           Old Virtual DOM Tree                   │  │
│  └──────────────────────────────────────────────────┘  │
│                        │                               │
│              Chỉ cập nhật phần thay đổi               │
│                        │                               │
│  ┌──────────────────────────────────────────────────┐  │
│  │                Real DOM                          │  │
│  └──────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘

Lợi ích:

  • Giảm thiểu DOM manipulation (tốn kém)
  • Batch updates - nhóm nhiều thay đổi lại
  • Diffing algorithm tìm thay đổi tối thiểu

Conditional Rendering

function UserStatus({ isLoggedIn, user }) {
  // 1. if-else với early return
  if (!isLoggedIn) {
    return <div>Please log in</div>;
  }

  // 2. Ternary operator
  return (
    <div>
      {isLoggedIn ? (
        <span>Welcome, {user.name}!</span>
      ) : (
        <span>Guest</span>
      )}
    </div>
  );
}

// 3. && short-circuit (hiển thị hoặc không)
function Notification({ hasMessage, message }) {
  return (
    <div>
      <h1>Dashboard</h1>
      {hasMessage && <div className="alert">{message}</div>}
    </div>
  );
}

// 4. Nullish coalescing ??
function UserAvatar({ avatarUrl }) {
  return (
    <img src={avatarUrl ?? '/default-avatar.png'} alt="avatar" />
  );
}

// 5. Switch statement qua object map
const statusComponents = {
  loading: <Spinner />,
  error: <ErrorMessage />,
  success: <DataTable />,
};

function DataView({ status }) {
  return statusComponents[status] ?? <div>Unknown status</div>;
}

List Rendering

// Luôn cần key prop khi render list
function ProductList({ products }) {
  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>
          <span>{product.name}</span>
          <span>${product.price}</span>
        </li>
      ))}
    </ul>
  );
}

// Key phải là unique và stable
// ✅ Dùng id từ data
<li key={item.id}>

// ❌ Tránh dùng index nếu list có thể thay đổi thứ tự
<li key={index}>  // Gây bug khi sort/filter

// Render nested lists
function CategoryList({ categories }) {
  return (
    <div>
      {categories.map((category) => (
        <div key={category.id}>
          <h3>{category.name}</h3>
          <ul>
            {category.items.map((item) => (
              <li key={item.id}>{item.name}</li>
            ))}
          </ul>
        </div>
      ))}
    </div>
  );
}

// Kết hợp filter và map
function ActiveUsers({ users }) {
  return (
    <ul>
      {users
        .filter((user) => user.isActive)
        .map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
    </ul>
  );
}

Fragments

import { Fragment } from 'react';

// Cú pháp đầy đủ
function TableRow({ item }) {
  return (
    <Fragment>
      <td>{item.name}</td>
      <td>{item.value}</td>
    </Fragment>
  );
}

// Short syntax <> - không hỗ trợ key
function List({ items }) {
  return (
    <>
      {items.map((item) => (
        // Phải dùng Fragment khi cần key
        <Fragment key={item.id}>
          <dt>{item.term}</dt>
          <dd>{item.description}</dd>
        </Fragment>
      ))}
    </>
  );
}

React 18 - Concurrent Rendering

import { startTransition, useDeferredValue } from 'react';

// startTransition - đánh dấu update không khẩn cấp
function SearchInput() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  function handleChange(e) {
    // Update UI ngay lập tức (khẩn cấp)
    setQuery(e.target.value);

    // Update kết quả tìm kiếm (không khẩn cấp)
    startTransition(() => {
      setResults(filterData(e.target.value));
    });
  }

  return (
    <>
      <input value={query} onChange={handleChange} />
      <SearchResults results={results} />
    </>
  );
}

// useDeferredValue - defer một value
function ProductSearch({ query }) {
  const deferredQuery = useDeferredValue(query);

  return <ProductList query={deferredQuery} />;
}

Components & Props

Functional Components

Functional components là cách hiện đại và được khuyến nghị để viết React components.

// Component đơn giản
function Greeting({ name }) {
  return <h1>Hello, {name}!</h1>;
}

// Arrow function syntax
const Button = ({ label, onClick, disabled = false }) => {
  return (
    <button onClick={onClick} disabled={disabled}>
      {label}
    </button>
  );
};

// Component với TypeScript
interface UserCardProps {
  id: number;
  name: string;
  email: string;
  avatarUrl?: string;
}

const UserCard = ({ id, name, email, avatarUrl }: UserCardProps) => {
  return (
    <div className="user-card">
      <img src={avatarUrl ?? '/default-avatar.png'} alt={name} />
      <h3>{name}</h3>
      <p>{email}</p>
    </div>
  );
};

Props

Cơ bản

// Truyền props
function App() {
  return (
    <UserCard
      id={1}
      name="Alice"
      email="alice@example.com"
      avatarUrl="/alice.png"
    />
  );
}

// Destructuring props
function Product({ name, price, category, inStock = true }) {
  return (
    <div>
      <h2>{name}</h2>
      <p>Price: ${price}</p>
      <p>Category: {category}</p>
      <p>Status: {inStock ? 'In Stock' : 'Out of Stock'}</p>
    </div>
  );
}

// Spread props
const buttonProps = { type: 'submit', disabled: false, className: 'btn-primary' };
<button {...buttonProps}>Submit</button>

Props đặc biệt

// children prop
function Card({ title, children }) {
  return (
    <div className="card">
      <h3 className="card-title">{title}</h3>
      <div className="card-body">{children}</div>
    </div>
  );
}

// Sử dụng
function App() {
  return (
    <Card title="My Card">
      <p>This is the card content</p>
      <button>Action</button>
    </Card>
  );
}

// render prop pattern
function DataFetcher({ url, render }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch(url).then(r => r.json()).then(setData);
  }, [url]);

  return render(data);
}

// Sử dụng
<DataFetcher
  url="/api/users"
  render={(data) => data ? <UserList users={data} /> : <Spinner />}
/>

Callback Props

// Truyền function qua props
function Counter({ onCountChange }) {
  const [count, setCount] = useState(0);

  function increment() {
    const newCount = count + 1;
    setCount(newCount);
    onCountChange?.(newCount); // Optional chaining
  }

  return (
    <div>
      <span>{count}</span>
      <button onClick={increment}>+</button>
    </div>
  );
}

// Parent component
function App() {
  function handleCountChange(newCount) {
    console.log('Count changed to:', newCount);
  }

  return <Counter onCountChange={handleCountChange} />;
}

Class Components (Legacy)

Cần biết để đọc code cũ. Trong dự án mới hãy dùng functional components.

import { Component } from 'react';

class UserProfile extends Component {
  constructor(props) {
    super(props);
    this.state = {
      isExpanded: false,
    };
    // Bind this nếu không dùng arrow function
    this.toggleExpand = this.toggleExpand.bind(this);
  }

  // Lifecycle methods
  componentDidMount() {
    // Gọi sau khi component mount (tương đương useEffect với [])
    this.fetchUserData();
  }

  componentDidUpdate(prevProps, prevState) {
    // Gọi khi props hoặc state thay đổi
    if (prevProps.userId !== this.props.userId) {
      this.fetchUserData();
    }
  }

  componentWillUnmount() {
    // Cleanup trước khi unmount
    this.subscription?.unsubscribe();
  }

  toggleExpand = () => {
    this.setState((prevState) => ({
      isExpanded: !prevState.isExpanded,
    }));
  };

  async fetchUserData() {
    const response = await fetch(`/api/users/${this.props.userId}`);
    const data = await response.json();
    this.setState({ user: data });
  }

  render() {
    const { user, isExpanded } = this.state;
    const { className } = this.props;

    return (
      <div className={className}>
        <h2>{user?.name}</h2>
        {isExpanded && <p>{user?.bio}</p>}
        <button onClick={this.toggleExpand}>
          {isExpanded ? 'Show Less' : 'Show More'}
        </button>
      </div>
    );
  }
}

So sánh Class vs Functional

Tính năngClass ComponentFunctional Component
SyntaxVerboseNgắn gọn
Statethis.stateuseState
LifecycleMethods cụ thểuseEffect
thisCần bindKhông có
Tái sử dụng logicHOC, Render PropsCustom Hooks
PerformanceTương đươngTương đương
Khuyến nghịLegacy✅ Hiện đại

Component Composition

// Tránh prop drilling với composition
// ❌ Prop drilling
function App({ user }) {
  return <Dashboard user={user} />;
}
function Dashboard({ user }) {
  return <Sidebar user={user} />;
}
function Sidebar({ user }) {
  return <UserMenu user={user} />;
}

// ✅ Composition pattern
function App({ user }) {
  const userMenu = <UserMenu user={user} />;
  return <Dashboard sidebar={<Sidebar>{userMenu}</Sidebar>} />;
}

// Slot pattern
function Layout({ header, sidebar, children, footer }) {
  return (
    <div className="layout">
      <header>{header}</header>
      <div className="content">
        <aside>{sidebar}</aside>
        <main>{children}</main>
      </div>
      <footer>{footer}</footer>
    </div>
  );
}

// Sử dụng
function App() {
  return (
    <Layout
      header={<NavBar />}
      sidebar={<SideNav />}
      footer={<Footer />}
    >
      <HomePage />
    </Layout>
  );
}

Controlled vs Uncontrolled Components

// Controlled - React quản lý state
function ControlledInput() {
  const [value, setValue] = useState('');

  return (
    <input
      value={value}
      onChange={(e) => setValue(e.target.value)}
    />
  );
}

// Uncontrolled - DOM quản lý state qua ref
function UncontrolledInput() {
  const inputRef = useRef(null);

  function handleSubmit() {
    console.log('Value:', inputRef.current.value);
  }

  return (
    <>
      <input ref={inputRef} defaultValue="initial" />
      <button onClick={handleSubmit}>Submit</button>
    </>
  );
}

Hooks Cơ bản

useState

Hook để quản lý state trong functional component.

import { useState } from 'react';

// State đơn giản
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
      <button onClick={() => setCount(count - 1)}>-</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

// State là object
function UserForm() {
  const [user, setUser] = useState({
    name: '',
    email: '',
    age: 0,
  });

  // Cập nhật một field - phải spread toàn bộ object
  function handleChange(field, value) {
    setUser((prev) => ({ ...prev, [field]: value }));
  }

  return (
    <form>
      <input
        value={user.name}
        onChange={(e) => handleChange('name', e.target.value)}
      />
      <input
        value={user.email}
        onChange={(e) => handleChange('email', e.target.value)}
      />
    </form>
  );
}

// Functional update - khi state mới phụ thuộc vào state cũ
function SafeCounter() {
  const [count, setCount] = useState(0);

  // ✅ Dùng functional update để tránh stale state
  function increment() {
    setCount((prev) => prev + 1);
  }

  // ❌ Có thể bị stale trong async context
  function badIncrement() {
    setCount(count + 1);
  }

  return <button onClick={increment}>Count: {count}</button>;
}

// Lazy initial state - tránh tính toán nặng mỗi lần render
function ExpensiveComponent() {
  // ✅ Hàm chỉ chạy một lần khi khởi tạo
  const [data, setData] = useState(() => computeExpensiveValue());

  // ❌ Tính toán mỗi lần render
  const [data2, setData2] = useState(computeExpensiveValue());

  return <div>{data}</div>;
}

useEffect

Hook để thực hiện side effects (fetch data, subscribe, timers, v.v.)

import { useState, useEffect } from 'react';

// Chạy sau mỗi lần render
useEffect(() => {
  console.log('Component rendered');
});

// Chạy một lần sau khi mount
useEffect(() => {
  console.log('Component mounted');
}, []);

// Chạy khi dependency thay đổi
useEffect(() => {
  console.log('userId changed:', userId);
}, [userId]);

// Cleanup function
useEffect(() => {
  const timer = setInterval(() => {
    setTime(new Date());
  }, 1000);

  // Cleanup: chạy trước khi effect chạy lại hoặc khi unmount
  return () => {
    clearInterval(timer);
  };
}, []);

// Fetch data pattern
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let isMounted = true; // Tránh update state sau khi unmount

    async function fetchUser() {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        if (!response.ok) throw new Error('Failed to fetch');
        const data = await response.json();

        if (isMounted) {
          setUser(data);
        }
      } catch (err) {
        if (isMounted) {
          setError(err.message);
        }
      } finally {
        if (isMounted) {
          setLoading(false);
        }
      }
    }

    fetchUser();

    return () => {
      isMounted = false;
    };
  }, [userId]);

  if (loading) return <Spinner />;
  if (error) return <ErrorMessage message={error} />;
  return <div>{user?.name}</div>;
}

// Event listener
useEffect(() => {
  function handleResize() {
    setWindowWidth(window.innerWidth);
  }

  window.addEventListener('resize', handleResize);
  return () => window.removeEventListener('resize', handleResize);
}, []);

// WebSocket subscription
useEffect(() => {
  const ws = new WebSocket('ws://localhost:8080');
  
  ws.onmessage = (event) => {
    setMessages((prev) => [...prev, event.data]);
  };

  return () => ws.close();
}, []);

Dependency Array - Những lỗi thường gặp

// ❌ Missing dependency
function BadComponent({ value }) {
  useEffect(() => {
    console.log(value); // value không ở trong dependency array
  }, []);
}

// ✅ Đúng
function GoodComponent({ value }) {
  useEffect(() => {
    console.log(value);
  }, [value]);
}

// Object/Array dependency - gây infinite loop
// ❌ Object mới mỗi lần render
function BadListComponent() {
  const options = { page: 1, size: 10 }; // Mới mỗi render

  useEffect(() => {
    fetchData(options);
  }, [options]); // Chạy mỗi render!
}

// ✅ Dùng primitive values
function GoodListComponent() {
  const page = 1;
  const size = 10;

  useEffect(() => {
    fetchData({ page, size });
  }, [page, size]);
}

useRef

Hook để lưu giá trị có thể thay đổi mà không gây re-render, hoặc truy cập DOM elements.

import { useRef, useEffect } from 'react';

// 1. Truy cập DOM element
function FocusInput() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current?.focus();
  }

  return (
    <>
      <input ref={inputRef} placeholder="Click button to focus" />
      <button onClick={handleClick}>Focus Input</button>
    </>
  );
}

// 2. Lưu giá trị không gây re-render
function Timer() {
  const [seconds, setSeconds] = useState(0);
  const intervalRef = useRef(null);

  function startTimer() {
    intervalRef.current = setInterval(() => {
      setSeconds((prev) => prev + 1);
    }, 1000);
  }

  function stopTimer() {
    clearInterval(intervalRef.current);
  }

  useEffect(() => {
    return () => clearInterval(intervalRef.current);
  }, []);

  return (
    <div>
      <p>{seconds}s</p>
      <button onClick={startTimer}>Start</button>
      <button onClick={stopTimer}>Stop</button>
    </div>
  );
}

// 3. Lưu previous value
function usePrevious(value) {
  const prevRef = useRef(undefined);

  useEffect(() => {
    prevRef.current = value;
  });

  return prevRef.current;
}

function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);

  return (
    <div>
      <p>Current: {count}, Previous: {prevCount}</p>
      <button onClick={() => setCount((c) => c + 1)}>+</button>
    </div>
  );
}

// 4. forwardRef - truyền ref xuống child component
const TextInput = forwardRef(function TextInput({ label, ...props }, ref) {
  return (
    <div>
      <label>{label}</label>
      <input ref={ref} {...props} />
    </div>
  );
});

// Sử dụng
function App() {
  const inputRef = useRef(null);

  return (
    <>
      <TextInput ref={inputRef} label="Name" />
      <button onClick={() => inputRef.current?.focus()}>Focus</button>
    </>
  );
}

useId

Hook để generate unique IDs, hữu ích cho accessibility và server-side rendering.

import { useId } from 'react';

function FormField({ label, type = 'text' }) {
  const id = useId();

  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <input id={id} type={type} />
    </div>
  );
}

// Multiple IDs từ một component
function PasswordField({ label }) {
  const id = useId();
  const descriptionId = `${id}-description`;

  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <input
        id={id}
        type="password"
        aria-describedby={descriptionId}
      />
      <p id={descriptionId}>Password must be at least 8 characters</p>
    </div>
  );
}

So sánh useState vs useRef

useStateuseRef
Gây re-render✅ Có❌ Không
Lưu qua renders✅ Có✅ Có
Tương tựStateInstance variable
Dùng khiCần UI cập nhậtKhông cần UI cập nhật
Ví dụForm valuesTimer ID, DOM refs

Hooks Nâng cao

useReducer

Thích hợp khi state logic phức tạp với nhiều sub-values hoặc state tiếp theo phụ thuộc vào state cũ.

import { useReducer } from 'react';

// Định nghĩa reducer
function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM': {
      const existingItem = state.items.find((i) => i.id === action.payload.id);
      if (existingItem) {
        return {
          ...state,
          items: state.items.map((i) =>
            i.id === action.payload.id
              ? { ...i, quantity: i.quantity + 1 }
              : i
          ),
        };
      }
      return {
        ...state,
        items: [...state.items, { ...action.payload, quantity: 1 }],
      };
    }

    case 'REMOVE_ITEM':
      return {
        ...state,
        items: state.items.filter((i) => i.id !== action.payload.id),
      };

    case 'CLEAR_CART':
      return { ...state, items: [] };

    case 'UPDATE_QUANTITY':
      return {
        ...state,
        items: state.items.map((i) =>
          i.id === action.payload.id
            ? { ...i, quantity: action.payload.quantity }
            : i
        ),
      };

    default:
      return state;
  }
}

const initialState = { items: [], discount: 0 };

function ShoppingCart() {
  const [cart, dispatch] = useReducer(cartReducer, initialState);

  const total = cart.items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );

  return (
    <div>
      <h2>Cart ({cart.items.length} items)</h2>
      {cart.items.map((item) => (
        <div key={item.id}>
          <span>{item.name} x{item.quantity}</span>
          <button onClick={() => dispatch({ type: 'REMOVE_ITEM', payload: { id: item.id } })}>
            Remove
          </button>
        </div>
      ))}
      <p>Total: ${total.toFixed(2)}</p>
      <button onClick={() => dispatch({ type: 'CLEAR_CART' })}>
        Clear Cart
      </button>
    </div>
  );
}

useState vs useReducer

Dùng useState khi:            Dùng useReducer khi:
- State đơn giản              - State là object phức tạp
- Ít logic cập nhật           - Nhiều actions khác nhau
- Không phụ thuộc vào nhau    - Logic cập nhật phức tạp
- Ví dụ: boolean, string      - Ví dụ: shopping cart, form

useMemo

Memoize giá trị tính toán tốn kém, chỉ tính lại khi dependencies thay đổi.

import { useMemo, useState } from 'react';

// ✅ Dùng useMemo khi tính toán nặng
function ProductList({ products, filters }) {
  const filteredProducts = useMemo(() => {
    return products
      .filter((p) => {
        if (filters.category && p.category !== filters.category) return false;
        if (filters.minPrice && p.price < filters.minPrice) return false;
        if (filters.maxPrice && p.price > filters.maxPrice) return false;
        return true;
      })
      .sort((a, b) => {
        if (filters.sortBy === 'price') return a.price - b.price;
        if (filters.sortBy === 'name') return a.name.localeCompare(b.name);
        return 0;
      });
  }, [products, filters]); // Chỉ tính lại khi products hoặc filters thay đổi

  return (
    <ul>
      {filteredProducts.map((p) => (
        <li key={p.id}>{p.name} - ${p.price}</li>
      ))}
    </ul>
  );
}

// ✅ Memoize reference để dùng trong useEffect dependency
function DataComponent({ config }) {
  const processedConfig = useMemo(() => ({
    apiUrl: config.baseUrl + '/api',
    timeout: config.timeout ?? 5000,
  }), [config.baseUrl, config.timeout]);

  useEffect(() => {
    fetchData(processedConfig);
  }, [processedConfig]); // processedConfig stable khi config không đổi
}

// ❌ Không cần useMemo cho tính toán đơn giản
const doubled = useMemo(() => count * 2, [count]); // Quá mức cần thiết
const doubled2 = count * 2; // Đủ rồi

useCallback

Memoize function reference, hữu ích khi truyền callback vào child component được memo hóa.

import { useCallback, memo } from 'react';

// Vấn đề: Function mới mỗi render gây re-render không cần thiết
function ParentBad() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  // ❌ Function mới mỗi lần parent render
  const handleDelete = (id) => {
    setItems((prev) => prev.filter((i) => i.id !== id));
  };

  return (
    <>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      {/* ItemList re-render mỗi khi name thay đổi do handleDelete mới */}
      <ItemList onDelete={handleDelete} />
    </>
  );
}

// ✅ Giải pháp với useCallback
function ParentGood() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  const handleDelete = useCallback((id) => {
    setItems((prev) => prev.filter((i) => i.id !== id));
  }, []); // Empty deps vì dùng functional update

  return (
    <>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      {/* ItemList KHÔNG re-render khi name thay đổi */}
      <MemoizedItemList onDelete={handleDelete} />
    </>
  );
}

// Child component được memo hóa
const MemoizedItemList = memo(function ItemList({ items, onDelete }) {
  console.log('ItemList rendered');
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          {item.name}
          <button onClick={() => onDelete(item.id)}>Delete</button>
        </li>
      ))}
    </ul>
  );
});

useLayoutEffect

Giống useEffect nhưng chạy đồng bộ sau khi DOM mutation, trước khi browser paint.

import { useLayoutEffect, useRef } from 'react';

// Dùng khi cần đọc layout từ DOM trước khi paint
function Tooltip({ text, targetRef }) {
  const tooltipRef = useRef(null);

  useLayoutEffect(() => {
    const tooltip = tooltipRef.current;
    const target = targetRef.current;

    if (!tooltip || !target) return;

    // Đọc vị trí DOM - cần đồng bộ để tránh flicker
    const targetRect = target.getBoundingClientRect();
    tooltip.style.top = `${targetRect.bottom + 8}px`;
    tooltip.style.left = `${targetRect.left}px`;
  });

  return (
    <div ref={tooltipRef} className="tooltip" role="tooltip">
      {text}
    </div>
  );
}
useEffect vs useLayoutEffect:

Browser: Render DOM  →  useLayoutEffect  →  Paint  →  useEffect
         (commit)         (sync)             (screen)   (async)

Dùng useLayoutEffect khi:
- Cần đọc/ghi DOM trước khi paint
- Animation, tooltip positioning
- Tránh visual flash

Dùng useEffect (thường) khi:
- Fetch data
- Event subscriptions
- Logging

Custom Hooks

Tái sử dụng stateful logic giữa các components.

// useLocalStorage - lưu state vào localStorage
function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });

  const setValue = useCallback((value) => {
    try {
      const valueToStore =
        value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error('Error saving to localStorage:', error);
    }
  }, [key, storedValue]);

  return [storedValue, setValue];
}

// Sử dụng
function Settings() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      Toggle theme: {theme}
    </button>
  );
}

// useFetch - data fetching
function useFetch(url) {
  const [state, setState] = useState({
    data: null,
    loading: true,
    error: null,
  });

  useEffect(() => {
    let cancelled = false;

    setState({ data: null, loading: true, error: null });

    fetch(url)
      .then((res) => {
        if (!res.ok) throw new Error(`HTTP error ${res.status}`);
        return res.json();
      })
      .then((data) => {
        if (!cancelled) setState({ data, loading: false, error: null });
      })
      .catch((error) => {
        if (!cancelled) setState({ data: null, loading: false, error });
      });

    return () => { cancelled = true; };
  }, [url]);

  return state;
}

// useDebounce - delay value update
function useDebounce(value, delay = 300) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

// Sử dụng useDebounce cho search
function SearchInput() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 300);

  const { data } = useFetch(
    debouncedQuery ? `/api/search?q=${debouncedQuery}` : null
  );

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      <SearchResults results={data} />
    </div>
  );
}

// useToggle - boolean toggle
function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);
  const toggle = useCallback(() => setValue((v) => !v), []);
  return [value, toggle];
}

// usePrevious - lưu giá trị trước đó
function usePrevious(value) {
  const ref = useRef(undefined);

  useEffect(() => {
    ref.current = value;
  });

  return ref.current;
}

// useWindowSize - reactive window dimensions
function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    const handleResize = () =>
      setSize({ width: window.innerWidth, height: window.innerHeight });

    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return size;
}

Rules of Hooks

// ✅ Gọi ở top level của component
function GoodComponent() {
  const [count, setCount] = useState(0); // OK
  const data = useFetch('/api/data');     // OK
}

// ❌ Không gọi trong điều kiện
function BadComponent({ show }) {
  if (show) {
    const [count] = useState(0); // LỖI! Không được trong if
  }
}

// ❌ Không gọi trong loop
function BadLoop() {
  for (let i = 0; i < 3; i++) {
    const [val] = useState(i); // LỖI!
  }
}

// ❌ Không gọi trong nested function thường
function BadNested() {
  function setupSomething() {
    const [val] = useState(0); // LỖI!
  }
}

// ✅ Được gọi trong custom hook
function useMyHook() {
  const [val] = useState(0); // OK - custom hook
  return val;
}

Context API

Khái niệm

Context API giải quyết vấn đề prop drilling - truyền props qua nhiều cấp component không cần thiết.

Không có Context (Prop Drilling):
App → Page → Section → Panel → Button (button cần theme)
     (pass)   (pass)   (pass)   (dùng)

Với Context:
App (Provider theme) ──────────────────→ Button (useContext)
     ↓              ↓               ↓
    Page          Section          Panel
   (không biết gì về theme)

createContext & useContext

import { createContext, useContext, useState, useMemo } from 'react';

// 1. Tạo context với default value
const ThemeContext = createContext({
  theme: 'light',
  toggleTheme: () => {},
});

// 2. Tạo Provider component
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
  };

  // Memoize value để tránh re-render không cần thiết
  const value = useMemo(
    () => ({ theme, toggleTheme }),
    [theme]
  );

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

// 3. Custom hook để sử dụng context (best practice)
function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

// 4. Sử dụng
function Header() {
  const { theme, toggleTheme } = useTheme();

  return (
    <header className={`header header--${theme}`}>
      <button onClick={toggleTheme}>
        Switch to {theme === 'light' ? 'dark' : 'light'} mode
      </button>
    </header>
  );
}

function App() {
  return (
    <ThemeProvider>
      <Header />
      <Main />
    </ThemeProvider>
  );
}

Auth Context Pattern

Ví dụ thực tế: quản lý authentication state.

import { createContext, useContext, useState, useCallback } from 'react';

interface User {
  id: number;
  name: string;
  email: string;
  roles: string[];
}

interface AuthContextType {
  user: User | null;
  isAuthenticated: boolean;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
}

const AuthContext = createContext<AuthContextType | null>(null);

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null);

  const login = useCallback(async (email: string, password: string) => {
    const response = await fetch('/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password }),
    });

    if (!response.ok) throw new Error('Login failed');

    const data = await response.json();
    setUser(data.user);
    localStorage.setItem('token', data.token);
  }, []);

  const logout = useCallback(() => {
    setUser(null);
    localStorage.removeItem('token');
  }, []);

  const value: AuthContextType = {
    user,
    isAuthenticated: user !== null,
    login,
    logout,
  };

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
}

// Sử dụng
function NavBar() {
  const { user, isAuthenticated, logout } = useAuth();

  return (
    <nav>
      {isAuthenticated ? (
        <>
          <span>Welcome, {user?.name}</span>
          <button onClick={logout}>Logout</button>
        </>
      ) : (
        <a href="/login">Login</a>
      )}
    </nav>
  );
}

Multiple Contexts

// Tách contexts theo chức năng
function App() {
  return (
    <AuthProvider>
      <ThemeProvider>
        <CartProvider>
          <Router>
            <AppContent />
          </Router>
        </CartProvider>
      </ThemeProvider>
    </AuthProvider>
  );
}

// Hoặc combine thành một AppProvider
function AppProvider({ children }) {
  return (
    <AuthProvider>
      <ThemeProvider>
        <CartProvider>
          {children}
        </CartProvider>
      </ThemeProvider>
    </AuthProvider>
  );
}

Context Performance - Tránh Re-renders không cần thiết

// ❌ Vấn đề: Mọi consumer re-render khi bất kỳ value nào thay đổi
const AppContext = createContext(null);

function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  const [cart, setCart] = useState([]);

  // Object mới mỗi render → mọi consumer re-render!
  return (
    <AppContext.Provider value={{ user, theme, cart, setUser, setTheme, setCart }}>
      {children}
    </AppContext.Provider>
  );
}

// ✅ Giải pháp 1: Tách contexts
const UserContext = createContext(null);
const ThemeContext = createContext(null);
const CartContext = createContext(null);

// ✅ Giải pháp 2: useMemo
function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');

  const userValue = useMemo(() => ({ user, setUser }), [user]);
  const themeValue = useMemo(() => ({ theme, setTheme }), [theme]);

  return (
    <UserContext.Provider value={userValue}>
      <ThemeContext.Provider value={themeValue}>
        {children}
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

Khi nào dùng Context vs Redux

Tiêu chíContext APIRedux Toolkit
Độ phức tạpĐơn giảnPhức tạp hơn
App sizeNhỏ-vừaLớn
DevToolsKhông có✅ Có
Time-travel debugKhông✅ Có
PerformanceCần tối ưu thủ côngĐược tối ưu
MiddlewareKhông✅ Có (Thunk, Saga)
Ví dụ phù hợpTheme, Auth, LanguageShopping cart, Complex state

Redux & Redux Toolkit

Khái niệm Cơ bản

Redux là state management library theo unidirectional data flow:

┌─────────────────────────────────────────────────────────────┐
│                    Redux Data Flow                          │
│                                                             │
│   Component  ──dispatch(action)──▶  Reducer  ──▶  Store    │
│      ▲                                                  │   │
│      └──────────────── state ───────────────────────────┘   │
│                                                             │
│   Action: { type: 'counter/increment', payload: 1 }        │
│   Reducer: Pure function (state, action) => newState        │
│   Store: Single source of truth                             │
└─────────────────────────────────────────────────────────────┘

Redux Toolkit (RTK) - Cách hiện đại

RTK là cách chính thức và được khuyến nghị để dùng Redux.

Cài đặt

npm install @reduxjs/toolkit react-redux

createSlice

// features/counter/counterSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

interface CounterState {
  value: number;
  status: 'idle' | 'loading' | 'failed';
}

const initialState: CounterState = {
  value: 0,
  status: 'idle',
};

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => {
      // RTK dùng Immer - có thể "mutate" state trực tiếp
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload;
    },
    reset: (state) => {
      state.value = 0;
    },
  },
});

// Export actions
export const { increment, decrement, incrementByAmount, reset } =
  counterSlice.actions;

// Export selectors
export const selectCount = (state: RootState) => state.counter.value;

export default counterSlice.reducer;

configureStore

// app/store.ts
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
import cartReducer from '../features/cart/cartSlice';

export const store = configureStore({
  reducer: {
    counter: counterReducer,
    cart: cartReducer,
  },
  // Middleware mặc định: thunk, serializability check
  // middleware: (getDefaultMiddleware) =>
  //   getDefaultMiddleware().concat(myMiddleware),
});

// TypeScript types
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

// Typed hooks
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

Provider Setup

// index.tsx / main.tsx
import { Provider } from 'react-redux';
import { store } from './app/store';
import App from './App';

createRoot(document.getElementById('root')!).render(
  <Provider store={store}>
    <App />
  </Provider>
);

Sử dụng trong Component

import { useAppDispatch, useAppSelector } from '../../app/store';
import { increment, decrement, selectCount } from './counterSlice';

function Counter() {
  const count = useAppSelector(selectCount);
  const dispatch = useAppDispatch();

  return (
    <div>
      <button onClick={() => dispatch(decrement())}>-</button>
      <span>{count}</span>
      <button onClick={() => dispatch(increment())}>+</button>
    </div>
  );
}

createAsyncThunk - Async Operations

// features/users/usersSlice.ts
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';

interface User {
  id: number;
  name: string;
  email: string;
}

interface UsersState {
  entities: User[];
  status: 'idle' | 'loading' | 'succeeded' | 'failed';
  error: string | null;
}

// Tạo async thunk
export const fetchUsers = createAsyncThunk(
  'users/fetchAll',
  async (_, { rejectWithValue }) => {
    try {
      const response = await fetch('/api/users');
      if (!response.ok) throw new Error('Server error');
      return await response.json() as User[];
    } catch (error) {
      return rejectWithValue((error as Error).message);
    }
  }
);

export const createUser = createAsyncThunk(
  'users/create',
  async (userData: Omit<User, 'id'>, { rejectWithValue }) => {
    try {
      const response = await fetch('/api/users', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(userData),
      });
      if (!response.ok) throw new Error('Failed to create user');
      return await response.json() as User;
    } catch (error) {
      return rejectWithValue((error as Error).message);
    }
  }
);

const usersSlice = createSlice({
  name: 'users',
  initialState: {
    entities: [],
    status: 'idle',
    error: null,
  } as UsersState,
  reducers: {
    userRemoved: (state, action: PayloadAction<number>) => {
      state.entities = state.entities.filter((u) => u.id !== action.payload);
    },
  },
  extraReducers: (builder) => {
    builder
      // fetchUsers
      .addCase(fetchUsers.pending, (state) => {
        state.status = 'loading';
        state.error = null;
      })
      .addCase(fetchUsers.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.entities = action.payload;
      })
      .addCase(fetchUsers.rejected, (state, action) => {
        state.status = 'failed';
        state.error = action.payload as string;
      })
      // createUser
      .addCase(createUser.fulfilled, (state, action) => {
        state.entities.push(action.payload);
      });
  },
});

export const { userRemoved } = usersSlice.actions;
export default usersSlice.reducer;

// Selectors
export const selectAllUsers = (state: RootState) => state.users.entities;
export const selectUsersStatus = (state: RootState) => state.users.status;
// Component sử dụng
function UserList() {
  const dispatch = useAppDispatch();
  const users = useAppSelector(selectAllUsers);
  const status = useAppSelector(selectUsersStatus);

  useEffect(() => {
    if (status === 'idle') {
      dispatch(fetchUsers());
    }
  }, [dispatch, status]);

  if (status === 'loading') return <Spinner />;
  if (status === 'failed') return <p>Error loading users</p>;

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>
          {user.name}
          <button onClick={() => dispatch(userRemoved(user.id))}>Delete</button>
        </li>
      ))}
    </ul>
  );
}

RTK Query - Data Fetching

RTK Query là giải pháp data fetching tích hợp trong Redux Toolkit.

// services/usersApi.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

export const usersApi = createApi({
  reducerPath: 'usersApi',
  baseQuery: fetchBaseQuery({
    baseUrl: '/api',
    prepareHeaders: (headers, { getState }) => {
      const token = (getState() as RootState).auth.token;
      if (token) {
        headers.set('Authorization', `Bearer ${token}`);
      }
      return headers;
    },
  }),
  tagTypes: ['User'],
  endpoints: (builder) => ({
    getUsers: builder.query<User[], void>({
      query: () => '/users',
      providesTags: ['User'],
    }),
    getUserById: builder.query<User, number>({
      query: (id) => `/users/${id}`,
      providesTags: (result, error, id) => [{ type: 'User', id }],
    }),
    createUser: builder.mutation<User, Omit<User, 'id'>>({
      query: (body) => ({
        url: '/users',
        method: 'POST',
        body,
      }),
      invalidatesTags: ['User'], // Tự động refetch users sau khi create
    }),
    updateUser: builder.mutation<User, Partial<User> & { id: number }>({
      query: ({ id, ...body }) => ({
        url: `/users/${id}`,
        method: 'PUT',
        body,
      }),
      invalidatesTags: (result, error, { id }) => [{ type: 'User', id }],
    }),
    deleteUser: builder.mutation<void, number>({
      query: (id) => ({ url: `/users/${id}`, method: 'DELETE' }),
      invalidatesTags: ['User'],
    }),
  }),
});

// Export hooks tự động generate
export const {
  useGetUsersQuery,
  useGetUserByIdQuery,
  useCreateUserMutation,
  useUpdateUserMutation,
  useDeleteUserMutation,
} = usersApi;
// Thêm vào store
export const store = configureStore({
  reducer: {
    [usersApi.reducerPath]: usersApi.reducer,
    // ...other reducers
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(usersApi.middleware),
});
// Sử dụng RTK Query hooks
function UserList() {
  const { data: users = [], isLoading, isError } = useGetUsersQuery();
  const [deleteUser] = useDeleteUserMutation();

  if (isLoading) return <Spinner />;
  if (isError) return <p>Error!</p>;

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>
          {user.name}
          <button onClick={() => deleteUser(user.id)}>Delete</button>
        </li>
      ))}
    </ul>
  );
}

// Conditional fetching
function UserProfile({ userId }: { userId: number | null }) {
  const { data: user } = useGetUserByIdQuery(userId!, {
    skip: userId === null, // Không fetch nếu userId null
  });

  return <div>{user?.name}</div>;
}

React Query (TanStack Query)

Giới thiệu

TanStack Query (trước đây là React Query) là thư viện server state management mạnh nhất hiện nay. Nó xử lý fetching, caching, synchronizing và updating server state.

Client State vs Server State:
- Client State: theme, language, UI state (dùng useState/Redux)
- Server State: API data, cần fetch, cache, sync (dùng React Query)

Cài đặt

npm install @tanstack/react-query @tanstack/react-query-devtools

Setup

// main.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,   // Data cũ sau 5 phút
      gcTime: 10 * 60 * 1000,     // Xóa cache sau 10 phút không dùng
      retry: 3,                    // Retry 3 lần khi fail
      refetchOnWindowFocus: true,  // Refetch khi focus lại tab
    },
  },
});

createRoot(document.getElementById('root')!).render(
  <QueryClientProvider client={queryClient}>
    <App />
    <ReactQueryDevtools initialIsOpen={false} />
  </QueryClientProvider>
);

useQuery - Fetching Data

import { useQuery } from '@tanstack/react-query';

// Query function
async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) throw new Error('Network response was not ok');
  return response.json();
}

// Sử dụng
function UserProfile({ userId }: { userId: number }) {
  const {
    data: user,
    isLoading,
    isError,
    error,
    isFetching,      // true khi đang fetch (kể cả background)
    isStale,         // true khi data đã cũ
    refetch,         // Function để refetch thủ công
  } = useQuery({
    queryKey: ['user', userId],   // Unique key - dùng để cache & invalidate
    queryFn: () => fetchUser(userId),
    enabled: userId > 0,          // Chỉ fetch khi userId hợp lệ
    staleTime: 30_000,            // Override global staleTime
    select: (data) => ({          // Transform data
      ...data,
      fullName: `${data.firstName} ${data.lastName}`,
    }),
  });

  if (isLoading) return <Skeleton />;
  if (isError) return <Alert message={(error as Error).message} />;

  return (
    <div>
      {isFetching && <span>Updating...</span>}
      <h1>{user?.fullName}</h1>
      <button onClick={() => refetch()}>Refresh</button>
    </div>
  );
}

Query Keys - Best Practices

// Query key xác định cache entry
// Khi key thay đổi → query mới được tạo

// Simple
useQuery({ queryKey: ['todos'], queryFn: getTodos });

// Với ID
useQuery({ queryKey: ['todo', todoId], queryFn: () => getTodo(todoId) });

// Với filters
useQuery({
  queryKey: ['todos', { status: 'active', page: 1 }],
  queryFn: () => getTodos({ status: 'active', page: 1 }),
});

// Query key factory pattern (best practice)
export const userKeys = {
  all: ['users'] as const,
  lists: () => [...userKeys.all, 'list'] as const,
  list: (filters: UserFilters) => [...userKeys.lists(), filters] as const,
  details: () => [...userKeys.all, 'detail'] as const,
  detail: (id: number) => [...userKeys.details(), id] as const,
};

useQuery({ queryKey: userKeys.detail(userId), queryFn: ... });

useMutation - Thay đổi Data

import { useMutation, useQueryClient } from '@tanstack/react-query';

async function createTodo(todo: CreateTodoDto): Promise<Todo> {
  const response = await fetch('/api/todos', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(todo),
  });
  if (!response.ok) throw new Error('Failed to create todo');
  return response.json();
}

function CreateTodoForm() {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: createTodo,

    // Optimistic update
    onMutate: async (newTodo) => {
      // Hủy queries đang chạy để tránh overwrite
      await queryClient.cancelQueries({ queryKey: ['todos'] });

      // Lưu snapshot state hiện tại
      const previousTodos = queryClient.getQueryData<Todo[]>(['todos']);

      // Optimistically update cache
      queryClient.setQueryData<Todo[]>(['todos'], (old = []) => [
        ...old,
        { id: Date.now(), ...newTodo, status: 'pending' },
      ]);

      return { previousTodos }; // Context để rollback
    },

    onError: (err, newTodo, context) => {
      // Rollback khi có lỗi
      queryClient.setQueryData(['todos'], context?.previousTodos);
      toast.error('Failed to create todo');
    },

    onSuccess: (data) => {
      // Invalidate để refetch
      queryClient.invalidateQueries({ queryKey: ['todos'] });
      toast.success('Todo created!');
    },

    onSettled: () => {
      // Chạy dù thành công hay thất bại
      queryClient.invalidateQueries({ queryKey: ['todos'] });
    },
  });

  function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    const form = e.target as HTMLFormElement;
    mutation.mutate({
      title: form.title.value,
      description: form.description.value,
    });
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="title" required />
      <textarea name="description" />
      <button type="submit" disabled={mutation.isPending}>
        {mutation.isPending ? 'Creating...' : 'Create Todo'}
      </button>
      {mutation.isError && (
        <p className="error">{(mutation.error as Error).message}</p>
      )}
    </form>
  );
}

Pagination

import { useQuery, keepPreviousData } from '@tanstack/react-query';

async function fetchTodos(page: number, pageSize: number) {
  const response = await fetch(
    `/api/todos?page=${page}&pageSize=${pageSize}`
  );
  return response.json() as Promise<{ items: Todo[]; total: number }>;
}

function TodoList() {
  const [page, setPage] = useState(1);
  const pageSize = 10;

  const { data, isLoading, isPlaceholderData } = useQuery({
    queryKey: ['todos', { page, pageSize }],
    queryFn: () => fetchTodos(page, pageSize),
    placeholderData: keepPreviousData, // Giữ data cũ khi fetch trang mới
  });

  const totalPages = Math.ceil((data?.total ?? 0) / pageSize);

  return (
    <div>
      {isLoading ? (
        <Spinner />
      ) : (
        <ul>
          {data?.items.map((todo) => (
            <li key={todo.id}>{todo.title}</li>
          ))}
        </ul>
      )}

      <div className="pagination">
        <button
          onClick={() => setPage((p) => Math.max(1, p - 1))}
          disabled={page === 1}
        >
          Previous
        </button>
        <span>
          Page {page} of {totalPages}
        </span>
        <button
          onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
          disabled={page === totalPages || isPlaceholderData}
        >
          Next
        </button>
      </div>
    </div>
  );
}

Infinite Scroll

import { useInfiniteQuery } from '@tanstack/react-query';
import { useIntersection } from '@mantine/hooks'; // hoặc custom hook

async function fetchPosts({ pageParam = 1 }) {
  const response = await fetch(`/api/posts?cursor=${pageParam}&limit=10`);
  return response.json() as Promise<{
    items: Post[];
    nextCursor: number | null;
  }>;
}

function InfinitePostList() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
    initialPageParam: 1,
    getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
  });

  const bottomRef = useRef<HTMLDivElement>(null);

  // Intersection Observer để auto-load
  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting && hasNextPage && !isFetchingNextPage) {
          fetchNextPage();
        }
      },
      { threshold: 0.1 }
    );

    if (bottomRef.current) observer.observe(bottomRef.current);
    return () => observer.disconnect();
  }, [fetchNextPage, hasNextPage, isFetchingNextPage]);

  const allPosts = data?.pages.flatMap((page) => page.items) ?? [];

  return (
    <div>
      {allPosts.map((post) => (
        <PostCard key={post.id} post={post} />
      ))}
      {isFetchingNextPage && <Spinner />}
      <div ref={bottomRef} className="h-4" />
    </div>
  );
}

prefetchQuery & Cache Management

const queryClient = useQueryClient();

// Prefetch trước khi user navigate
async function prefetchUser(userId: number) {
  await queryClient.prefetchQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
    staleTime: 10_000,
  });
}

// Invalidate - buộc refetch
queryClient.invalidateQueries({ queryKey: ['users'] });

// Set data trực tiếp
queryClient.setQueryData(['user', 1], updatedUser);

// Xóa cache
queryClient.removeQueries({ queryKey: ['user', userId] });

// Hover prefetch pattern
function UserLink({ userId, name }: { userId: number; name: string }) {
  const queryClient = useQueryClient();

  return (
    <a
      href={`/users/${userId}`}
      onMouseEnter={() => prefetchUser(userId)} // Prefetch khi hover
    >
      {name}
    </a>
  );
}

React Router v6

Cài đặt & Setup

npm install react-router-dom
// main.tsx
import { BrowserRouter } from 'react-router-dom';

createRoot(document.getElementById('root')!).render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

Routes Cơ bản

import { Routes, Route, Link, NavLink } from 'react-router-dom';

function App() {
  return (
    <div>
      {/* Navigation */}
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>

        {/* NavLink tự động thêm class "active" */}
        <NavLink
          to="/products"
          className={({ isActive }) => isActive ? 'nav-active' : ''}
        >
          Products
        </NavLink>
      </nav>

      {/* Routes */}
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/products" element={<Products />} />
        <Route path="/products/:productId" element={<ProductDetail />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </div>
  );
}

URL Parameters & Query Strings

import { useParams, useSearchParams } from 'react-router-dom';

// URL: /products/42
function ProductDetail() {
  const { productId } = useParams<{ productId: string }>();

  return <div>Product ID: {productId}</div>;
}

// URL: /products?category=electronics&page=2
function ProductList() {
  const [searchParams, setSearchParams] = useSearchParams();

  const category = searchParams.get('category') ?? 'all';
  const page = Number(searchParams.get('page') ?? '1');

  function handleCategoryChange(newCategory: string) {
    setSearchParams((prev) => {
      prev.set('category', newCategory);
      prev.set('page', '1'); // Reset page khi đổi category
      return prev;
    });
  }

  return (
    <div>
      <select
        value={category}
        onChange={(e) => handleCategoryChange(e.target.value)}
      >
        <option value="all">All</option>
        <option value="electronics">Electronics</option>
        <option value="clothing">Clothing</option>
      </select>
      <p>Category: {category}, Page: {page}</p>
    </div>
  );
}

Nested Routes

// Layout component với Outlet
import { Outlet, Link } from 'react-router-dom';

function DashboardLayout() {
  return (
    <div className="dashboard">
      <aside>
        <Link to="/dashboard">Overview</Link>
        <Link to="/dashboard/analytics">Analytics</Link>
        <Link to="/dashboard/settings">Settings</Link>
      </aside>
      <main>
        <Outlet /> {/* Render child route content */}
      </main>
    </div>
  );
}

// Route config
function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/dashboard" element={<DashboardLayout />}>
        <Route index element={<DashboardOverview />} />         {/* /dashboard */}
        <Route path="analytics" element={<Analytics />} />     {/* /dashboard/analytics */}
        <Route path="settings" element={<Settings />} />       {/* /dashboard/settings */}
        <Route path="users/:userId" element={<UserDetail />} />{/* /dashboard/users/:userId */}
      </Route>
    </Routes>
  );
}

Protected Routes

import { Navigate, Outlet, useLocation } from 'react-router-dom';
import { useAuth } from './hooks/useAuth';

// Protected Route component
function ProtectedRoute({ requiredRole }: { requiredRole?: string }) {
  const { isAuthenticated, user } = useAuth();
  const location = useLocation();

  if (!isAuthenticated) {
    // Lưu location để redirect sau khi login
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  if (requiredRole && !user?.roles.includes(requiredRole)) {
    return <Navigate to="/unauthorized" replace />;
  }

  return <Outlet />;
}

// Sử dụng
function App() {
  return (
    <Routes>
      {/* Public routes */}
      <Route path="/login" element={<Login />} />
      <Route path="/register" element={<Register />} />

      {/* Protected routes */}
      <Route element={<ProtectedRoute />}>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/profile" element={<Profile />} />
      </Route>

      {/* Admin only */}
      <Route element={<ProtectedRoute requiredRole="admin" />}>
        <Route path="/admin" element={<AdminPanel />} />
      </Route>
    </Routes>
  );
}

// Login redirect sau khi đăng nhập thành công
function Login() {
  const navigate = useNavigate();
  const location = useLocation();
  const { login } = useAuth();

  const from = (location.state as { from?: Location })?.from?.pathname ?? '/dashboard';

  async function handleSubmit(credentials: Credentials) {
    await login(credentials);
    navigate(from, { replace: true }); // Redirect về trang trước
  }

  return <LoginForm onSubmit={handleSubmit} />;
}

Programmatic Navigation

import { useNavigate } from 'react-router-dom';

function ProductForm() {
  const navigate = useNavigate();

  async function handleSubmit(data: ProductData) {
    const product = await createProduct(data);
    
    // Navigate với state
    navigate(`/products/${product.id}`, {
      replace: false,   // Thêm vào history (default)
      state: { message: 'Product created successfully!' },
    });
  }

  return (
    <form onSubmit={handleSubmit}>
      {/* ... */}
      <button type="button" onClick={() => navigate(-1)}>
        Go Back
      </button>
      <button type="button" onClick={() => navigate('/products')}>
        Cancel
      </button>
    </form>
  );
}

// Đọc state từ navigation
function ProductDetail() {
  const location = useLocation();
  const message = (location.state as { message?: string })?.message;

  return (
    <div>
      {message && <Alert>{message}</Alert>}
      {/* ... */}
    </div>
  );
}

Lazy Loading Routes

import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';

// Lazy load components
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Analytics = lazy(() => import('./pages/Analytics'));
const AdminPanel = lazy(() => import('./pages/AdminPanel'));

function App() {
  return (
    <Suspense fallback={<PageLoader />}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/analytics" element={<Analytics />} />
        <Route path="/admin" element={<AdminPanel />} />
      </Routes>
    </Suspense>
  );
}

Link / NavLinkuseNavigate
Dùng khiNavigation trong JSXNavigation trong event handlers
Accessibility✅ Semantic <a> tag❌ Cần thêm manually
Ví dụMenu items, buttonsForm submit, callback

Forms & Validation

Controlled Forms - Cơ bản

import { useState, FormEvent } from 'react';

interface LoginForm {
  email: string;
  password: string;
  rememberMe: boolean;
}

function LoginForm() {
  const [form, setForm] = useState<LoginForm>({
    email: '',
    password: '',
    rememberMe: false,
  });
  const [errors, setErrors] = useState<Partial<LoginForm>>({});

  function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    const { name, value, type, checked } = e.target;
    setForm((prev) => ({
      ...prev,
      [name]: type === 'checkbox' ? checked : value,
    }));
    // Clear error khi user sửa
    setErrors((prev) => ({ ...prev, [name]: undefined }));
  }

  function validate(): boolean {
    const newErrors: Partial<Record<keyof LoginForm, string>> = {};

    if (!form.email) newErrors.email = 'Email is required';
    else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email))
      newErrors.email = 'Invalid email format';

    if (!form.password) newErrors.password = 'Password is required';
    else if (form.password.length < 8)
      newErrors.password = 'Password must be at least 8 characters';

    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  }

  async function handleSubmit(e: FormEvent) {
    e.preventDefault();
    if (!validate()) return;

    await login(form);
  }

  return (
    <form onSubmit={handleSubmit} noValidate>
      <div>
        <label htmlFor="email">Email</label>
        <input
          id="email"
          name="email"
          type="email"
          value={form.email}
          onChange={handleChange}
          aria-invalid={!!errors.email}
          aria-describedby={errors.email ? 'email-error' : undefined}
        />
        {errors.email && (
          <span id="email-error" role="alert">{errors.email}</span>
        )}
      </div>

      <div>
        <label htmlFor="password">Password</label>
        <input
          id="password"
          name="password"
          type="password"
          value={form.password}
          onChange={handleChange}
          aria-invalid={!!errors.password}
        />
        {errors.password && <span role="alert">{errors.password}</span>}
      </div>

      <label>
        <input
          name="rememberMe"
          type="checkbox"
          checked={form.rememberMe}
          onChange={handleChange}
        />
        Remember me
      </label>

      <button type="submit">Login</button>
    </form>
  );
}

React Hook Form

Thư viện form mạnh nhất, hiệu suất cao vì không re-render mỗi keystroke.

npm install react-hook-form
import { useForm, SubmitHandler } from 'react-hook-form';

interface RegisterForm {
  username: string;
  email: string;
  password: string;
  confirmPassword: string;
  age: number;
}

function RegisterForm() {
  const {
    register,          // Kết nối input với form
    handleSubmit,      // Wrap submit handler
    formState: { errors, isSubmitting, isValid },
    watch,             // Theo dõi giá trị field
    reset,             // Reset form
    setError,          // Set error thủ công
    getValues,         // Lấy giá trị hiện tại
  } = useForm<RegisterForm>({
    mode: 'onBlur',    // Validate khi blur (tùy chọn: onChange, onSubmit)
    defaultValues: {
      username: '',
      email: '',
      password: '',
      confirmPassword: '',
      age: 18,
    },
  });

  const password = watch('password'); // Theo dõi giá trị password

  const onSubmit: SubmitHandler<RegisterForm> = async (data) => {
    try {
      await registerUser(data);
      reset();
    } catch (err) {
      // Set server-side error
      setError('email', {
        type: 'server',
        message: 'Email already exists',
      });
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} noValidate>
      <div>
        <label>Username</label>
        <input
          {...register('username', {
            required: 'Username is required',
            minLength: { value: 3, message: 'Minimum 3 characters' },
            maxLength: { value: 20, message: 'Maximum 20 characters' },
            pattern: {
              value: /^[a-zA-Z0-9_]+$/,
              message: 'Only letters, numbers and underscore',
            },
          })}
        />
        {errors.username && <span>{errors.username.message}</span>}
      </div>

      <div>
        <label>Email</label>
        <input
          type="email"
          {...register('email', {
            required: 'Email is required',
            pattern: {
              value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
              message: 'Invalid email',
            },
          })}
        />
        {errors.email && <span>{errors.email.message}</span>}
      </div>

      <div>
        <label>Password</label>
        <input
          type="password"
          {...register('password', {
            required: 'Password is required',
            minLength: { value: 8, message: 'Minimum 8 characters' },
          })}
        />
        {errors.password && <span>{errors.password.message}</span>}
      </div>

      <div>
        <label>Confirm Password</label>
        <input
          type="password"
          {...register('confirmPassword', {
            required: 'Please confirm password',
            validate: (value) =>
              value === password || 'Passwords do not match',
          })}
        />
        {errors.confirmPassword && (
          <span>{errors.confirmPassword.message}</span>
        )}
      </div>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Registering...' : 'Register'}
      </button>
    </form>
  );
}

React Hook Form + Zod

Kết hợp React Hook Form với Zod schema validation.

npm install zod @hookform/resolvers
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

// Định nghĩa schema
const productSchema = z.object({
  name: z
    .string()
    .min(1, 'Name is required')
    .max(100, 'Name too long'),
  price: z
    .number({ invalid_type_error: 'Price must be a number' })
    .positive('Price must be positive')
    .multipleOf(0.01, 'Max 2 decimal places'),
  category: z.enum(['electronics', 'clothing', 'food'], {
    errorMap: () => ({ message: 'Please select a category' }),
  }),
  description: z.string().optional(),
  inStock: z.boolean().default(true),
  tags: z.array(z.string()).min(1, 'At least one tag required'),
});

// Infer TypeScript type từ schema
type ProductFormData = z.infer<typeof productSchema>;

function ProductForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
    control,
  } = useForm<ProductFormData>({
    resolver: zodResolver(productSchema),
    defaultValues: {
      inStock: true,
      tags: [],
    },
  });

  const onSubmit = async (data: ProductFormData) => {
    // data đã được validated và typed!
    await createProduct(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <input
          placeholder="Product name"
          {...register('name')}
        />
        {errors.name && <span>{errors.name.message}</span>}
      </div>

      <div>
        <input
          type="number"
          step="0.01"
          placeholder="Price"
          {...register('price', { valueAsNumber: true })}
        />
        {errors.price && <span>{errors.price.message}</span>}
      </div>

      <div>
        <select {...register('category')}>
          <option value="">Select category</option>
          <option value="electronics">Electronics</option>
          <option value="clothing">Clothing</option>
          <option value="food">Food</option>
        </select>
        {errors.category && <span>{errors.category.message}</span>}
      </div>

      <label>
        <input type="checkbox" {...register('inStock')} />
        In Stock
      </label>

      <button type="submit">Save Product</button>
    </form>
  );
}

useFieldArray - Dynamic Fields

import { useForm, useFieldArray, Controller } from 'react-hook-form';

interface OrderForm {
  customerName: string;
  items: {
    productId: string;
    quantity: number;
    price: number;
  }[];
}

function OrderForm() {
  const { register, control, handleSubmit, watch } = useForm<OrderForm>({
    defaultValues: {
      customerName: '',
      items: [{ productId: '', quantity: 1, price: 0 }],
    },
  });

  const { fields, append, remove, move } = useFieldArray({
    control,
    name: 'items',
  });

  const items = watch('items');
  const total = items.reduce((sum, item) => sum + item.quantity * item.price, 0);

  return (
    <form onSubmit={handleSubmit(console.log)}>
      <input
        placeholder="Customer name"
        {...register('customerName', { required: true })}
      />

      {fields.map((field, index) => (
        <div key={field.id} className="order-item">
          <input
            placeholder="Product ID"
            {...register(`items.${index}.productId`, { required: true })}
          />
          <input
            type="number"
            min={1}
            {...register(`items.${index}.quantity`, {
              valueAsNumber: true,
              min: 1,
            })}
          />
          <input
            type="number"
            step="0.01"
            {...register(`items.${index}.price`, { valueAsNumber: true })}
          />
          <button
            type="button"
            onClick={() => remove(index)}
            disabled={fields.length === 1}
          >
            Remove
          </button>
        </div>
      ))}

      <button
        type="button"
        onClick={() => append({ productId: '', quantity: 1, price: 0 })}
      >
        + Add Item
      </button>

      <p>Total: ${total.toFixed(2)}</p>

      <button type="submit">Place Order</button>
    </form>
  );
}

Styling trong React

CSS Modules

CSS Modules tạo scoped CSS, tránh conflict tên class.

// Button.module.css
.button {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.primary {
  background-color: #007bff;
  color: white;
}

.secondary {
  background-color: #6c757d;
  color: white;
}

.button:hover {
  opacity: 0.9;
}
// Button.tsx
import styles from './Button.module.css';
import clsx from 'clsx'; // hoặc classnames

interface ButtonProps {
  variant?: 'primary' | 'secondary';
  children: React.ReactNode;
  onClick?: () => void;
}

function Button({ variant = 'primary', children, onClick }: ButtonProps) {
  return (
    <button
      className={clsx(styles.button, styles[variant])}
      onClick={onClick}
    >
      {children}
    </button>
  );
}

// Class tên được hash: button_primary__xHk3a (tự động unique)

Tailwind CSS

Utility-first CSS framework, được dùng phổ biến nhất hiện nay.

npm install tailwindcss @tailwindcss/vite
// Component với Tailwind
function ProductCard({ product }: { product: Product }) {
  return (
    <div className="rounded-lg border border-gray-200 p-4 shadow-sm hover:shadow-md transition-shadow">
      <img
        src={product.imageUrl}
        alt={product.name}
        className="h-48 w-full object-cover rounded-md"
      />
      <div className="mt-3">
        <h3 className="text-lg font-semibold text-gray-900">{product.name}</h3>
        <p className="mt-1 text-sm text-gray-500">{product.description}</p>
        <div className="mt-3 flex items-center justify-between">
          <span className="text-xl font-bold text-blue-600">
            ${product.price}
          </span>
          <button className="rounded-md bg-blue-600 px-4 py-2 text-sm text-white hover:bg-blue-700 disabled:opacity-50">
            Add to Cart
          </button>
        </div>
      </div>
    </div>
  );
}

clsx / tailwind-merge - Conditional Classes

npm install clsx tailwind-merge
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';

// Utility function (nên tạo trong lib/utils.ts)
function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

// Sử dụng
interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'danger';
  size?: 'sm' | 'md' | 'lg';
  disabled?: boolean;
  className?: string;
  children: React.ReactNode;
}

function Button({
  variant = 'primary',
  size = 'md',
  disabled,
  className,
  children,
}: ButtonProps) {
  return (
    <button
      disabled={disabled}
      className={cn(
        // Base styles
        'inline-flex items-center justify-center rounded-md font-medium transition-colors focus:outline-none focus:ring-2',

        // Variant styles
        {
          'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500':
            variant === 'primary',
          'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500':
            variant === 'secondary',
          'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500':
            variant === 'danger',
        },

        // Size styles
        {
          'h-8 px-3 text-sm': size === 'sm',
          'h-10 px-4 text-base': size === 'md',
          'h-12 px-6 text-lg': size === 'lg',
        },

        // Disabled
        { 'cursor-not-allowed opacity-50': disabled },

        // Allow className override
        className
      )}
    >
      {children}
    </button>
  );
}

styled-components

CSS-in-JS, tạo styled components với template literals.

npm install styled-components
npm install -D @types/styled-components
import styled, { css, ThemeProvider, createGlobalStyle } from 'styled-components';

// Global styles
const GlobalStyle = createGlobalStyle`
  *, *::before, *::after {
    box-sizing: border-box;
  }
  
  body {
    margin: 0;
    font-family: 'Inter', sans-serif;
  }
`;

// Theme
const theme = {
  colors: {
    primary: '#007bff',
    secondary: '#6c757d',
    danger: '#dc3545',
    background: '#f8f9fa',
  },
  spacing: (n: number) => `${n * 4}px`,
  borderRadius: '6px',
};

// Styled components
const Card = styled.div`
  background: white;
  border-radius: ${({ theme }) => theme.borderRadius};
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  padding: ${({ theme }) => theme.spacing(4)};
`;

interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'danger';
  size?: 'sm' | 'md' | 'lg';
}

const Button = styled.button<ButtonProps>`
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-weight: 500;
  transition: opacity 0.2s;

  &:hover {
    opacity: 0.9;
  }

  &:disabled {
    cursor: not-allowed;
    opacity: 0.5;
  }

  ${({ variant = 'primary', theme }) =>
    variant === 'primary'
      ? css`
          background: ${theme.colors.primary};
          color: white;
        `
      : variant === 'danger'
      ? css`
          background: ${theme.colors.danger};
          color: white;
        `
      : css`
          background: ${theme.colors.secondary};
          color: white;
        `}

  ${({ size = 'md' }) =>
    size === 'sm'
      ? css`padding: 4px 12px; font-size: 14px;`
      : size === 'lg'
      ? css`padding: 12px 24px; font-size: 18px;`
      : css`padding: 8px 16px; font-size: 16px;`}
`;

// Extend styled component
const PrimaryButton = styled(Button).attrs({ variant: 'primary' })`
  text-transform: uppercase;
  letter-spacing: 0.5px;
`;

// App với ThemeProvider
function App() {
  return (
    <ThemeProvider theme={theme}>
      <GlobalStyle />
      <Card>
        <h2>Product</h2>
        <Button variant="primary">Add to Cart</Button>
        <Button variant="danger" size="sm">Delete</Button>
      </Card>
    </ThemeProvider>
  );
}

So sánh các Styling Approaches

CSS ModulesTailwind CSSstyled-components
Bundle sizeNhỏNhỏ (purge)Lớn hơn
DXTốtRất tốtTốt
Type-safeHạn chế✅ (với TS)
ThemingThủ côngConfig fileTheme object
Runtime✅ CSS-in-JS
Learning curveThấpTrung bìnhTrung bình
Phù hợpMọi dự ánDự án mớiComponent library

Khuyến nghị

  • Dự án mới: Tailwind CSS + clsx/tw-merge
  • Component library: styled-components hoặc vanilla-extract
  • Legacy codebase: CSS Modules

Performance Optimization

React.memo

Ngăn component re-render khi props không thay đổi (shallow comparison).

import { memo, useState } from 'react';

interface ProductCardProps {
  id: number;
  name: string;
  price: number;
  onAddToCart: (id: number) => void;
}

// ✅ Wrap component với memo
const ProductCard = memo(function ProductCard({
  id,
  name,
  price,
  onAddToCart,
}: ProductCardProps) {
  console.log(`Rendering ProductCard ${id}`);

  return (
    <div className="product-card">
      <h3>{name}</h3>
      <p>${price}</p>
      <button onClick={() => onAddToCart(id)}>Add to Cart</button>
    </div>
  );
});

// Khi nào memo có tác dụng?
function App() {
  const [cartCount, setCartCount] = useState(0);
  const [products] = useState([...]);

  // ❌ Handler mới mỗi render → memo vô tác dụng!
  // const handleAddToCart = (id) => setCartCount(c => c + 1);

  // ✅ Stable reference với useCallback
  const handleAddToCart = useCallback((id: number) => {
    setCartCount((c) => c + 1);
    addToCart(id);
  }, []);

  return (
    <div>
      <p>Cart: {cartCount}</p>
      {products.map((p) => (
        <ProductCard
          key={p.id}
          {...p}
          onAddToCart={handleAddToCart}
        />
      ))}
    </div>
  );
}

// Custom comparison function
const MemoizedList = memo(
  function HeavyList({ items, searchQuery }) { ... },
  (prevProps, nextProps) => {
    // true = không re-render, false = re-render
    return (
      prevProps.searchQuery === nextProps.searchQuery &&
      prevProps.items.length === nextProps.items.length
    );
  }
);

Code Splitting & Lazy Loading

Chia nhỏ bundle, chỉ load code khi cần.

import { lazy, Suspense, startTransition } from 'react';

// Lazy load pages (route-based splitting)
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Analytics = lazy(() => import('./pages/Analytics'));
const Settings = lazy(() => import('./pages/Settings'));

// Lazy load heavy components
const RichTextEditor = lazy(() => import('./components/RichTextEditor'));
const DataGrid = lazy(() =>
  import('./components/DataGrid').then((module) => ({
    default: module.DataGrid, // Named export
  }))
);

function App() {
  const [showEditor, setShowEditor] = useState(false);

  return (
    <Suspense fallback={<div className="page-spinner">Loading...</div>}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/analytics" element={<Analytics />} />
      </Routes>

      <button
        onClick={() => startTransition(() => setShowEditor(true))}
      >
        Open Editor
      </button>

      {showEditor && (
        <Suspense fallback={<div>Loading editor...</div>}>
          <RichTextEditor />
        </Suspense>
      )}
    </Suspense>
  );
}

Virtualization - Render Danh sách Lớn

Chỉ render các items hiển thị trong viewport.

npm install @tanstack/react-virtual
import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';

function VirtualList({ items }: { items: Item[] }) {
  const parentRef = useRef<HTMLDivElement>(null);

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 60, // Estimated row height
    overscan: 5,            // Render thêm items ngoài viewport
  });

  return (
    <div
      ref={parentRef}
      style={{ height: '600px', overflow: 'auto' }}
    >
      {/* Total height container */}
      <div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
        {virtualizer.getVirtualItems().map((virtualItem) => (
          <div
            key={virtualItem.key}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              height: `${virtualItem.size}px`,
              transform: `translateY(${virtualItem.start}px)`,
            }}
          >
            <ItemRow item={items[virtualItem.index]} />
          </div>
        ))}
      </div>
    </div>
  );
}

Tránh Re-renders Không Cần Thiết

// 1. State colocation - đặt state gần nơi dùng
// ❌ State ở root khiến toàn bộ app re-render
function App() {
  const [inputValue, setInputValue] = useState(''); // Không cần ở đây
  return (
    <>
      <SearchBar value={inputValue} onChange={setInputValue} />
      <HeavyComponent />  {/* Re-render mỗi khi type */}
    </>
  );
}

// ✅ State trong component cần nó
function App() {
  return (
    <>
      <SearchBar /> {/* Tự quản lý state */}
      <HeavyComponent /> {/* Không re-render */}
    </>
  );
}

// 2. Children trick - tránh re-render khi parent thay đổi
function SlowParent({ children }) {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount((c) => c + 1)}>
        Count: {count}
      </button>
      {children} {/* children không re-render khi count thay đổi */}
    </div>
  );
}

function App() {
  return (
    <SlowParent>
      <HeavyComponent /> {/* Không bị ảnh hưởng bởi SlowParent state */}
    </SlowParent>
  );
}

// 3. Tách components nhỏ
// ❌ Cả form re-render khi count thay đổi
function ProductPage() {
  const [count, setCount] = useState(0);
  const [formData, setFormData] = useState({...});

  return (
    <div>
      <Counter count={count} onChange={setCount} />
      <HeavyForm formData={formData} onChange={setFormData} />
    </div>
  );
}

React Profiler

import { Profiler, ProfilerOnRenderCallback } from 'react';

const onRender: ProfilerOnRenderCallback = (
  id,           // Component tree identifier
  phase,        // "mount" hoặc "update"
  actualDuration, // Thời gian render (ms)
  baseDuration,   // Ước tính không có memo
  startTime,
  commitTime
) => {
  if (actualDuration > 16) { // Slower than 60fps
    console.warn(`Slow render in ${id}: ${actualDuration.toFixed(2)}ms`);
  }
};

function App() {
  return (
    <Profiler id="ProductList" onRender={onRender}>
      <ProductList />
    </Profiler>
  );
}

Web Workers cho Tính toán Nặng

// worker.ts
self.onmessage = (e: MessageEvent) => {
  const { data } = e;
  const result = heavyCalculation(data);
  self.postMessage(result);
};

// Component
function DataAnalysis({ rawData }) {
  const [result, setResult] = useState(null);

  useEffect(() => {
    const worker = new Worker(
      new URL('./worker.ts', import.meta.url),
      { type: 'module' }
    );

    worker.postMessage(rawData);
    worker.onmessage = (e) => {
      setResult(e.data);
      worker.terminate();
    };

    return () => worker.terminate();
  }, [rawData]);

  return result ? <Chart data={result} /> : <Spinner />;
}

Checklist Performance

□ Dùng React DevTools Profiler để identify bottlenecks
□ Áp dụng React.memo cho components render nhiều
□ useCallback cho callbacks truyền vào memo components
□ useMemo cho tính toán nặng
□ Route-based code splitting với lazy()
□ Virtualize danh sách dài (>100 items)
□ Tránh tạo object/array mới trong JSX (gây re-render)
□ Đặt state gần nơi sử dụng (state colocation)
□ Dùng Suspense boundaries hợp lý
□ Optimize images (lazy loading, correct size)
□ Enable gzip/brotli compression ở server

Testing React Apps

Cài đặt

# Với Vite
npm install -D vitest @testing-library/react @testing-library/user-event @testing-library/jest-dom jsdom

# Hoặc với Create React App (Jest có sẵn)
npm install -D @testing-library/react @testing-library/user-event @testing-library/jest-dom
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: './src/test/setup.ts',
  },
});

// src/test/setup.ts
import '@testing-library/jest-dom';

React Testing Library - Nguyên tắc

“Test your components the way users use them.”

// ✅ Query theo những gì user thấy
screen.getByRole('button', { name: /submit/i });
screen.getByLabelText('Email');
screen.getByText('Welcome, Alice!');
screen.getByPlaceholderText('Search...');

// ✅ Khi không có accessible text
screen.getByTestId('loading-spinner');

// ❌ Tránh query theo implementation details
screen.getByClassName('btn-primary');   // Fragile
screen.getByAttribute('data-id', '42'); // Fragile

Unit Testing Components

// Button.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from './Button';

describe('Button', () => {
  it('renders with correct text', () => {
    render(<Button>Click me</Button>);
    expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();
  });

  it('calls onClick when clicked', async () => {
    const user = userEvent.setup();
    const handleClick = vi.fn();

    render(<Button onClick={handleClick}>Submit</Button>);

    await user.click(screen.getByRole('button', { name: /submit/i }));
    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('is disabled when disabled prop is true', () => {
    render(<Button disabled>Submit</Button>);
    expect(screen.getByRole('button')).toBeDisabled();
  });

  it('does not call onClick when disabled', async () => {
    const user = userEvent.setup();
    const handleClick = vi.fn();

    render(<Button disabled onClick={handleClick}>Submit</Button>);
    await user.click(screen.getByRole('button'));

    expect(handleClick).not.toHaveBeenCalled();
  });
});

Testing Forms

// LoginForm.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from './LoginForm';

describe('LoginForm', () => {
  const user = userEvent.setup();

  it('shows validation errors for empty submit', async () => {
    render(<LoginForm onSubmit={vi.fn()} />);

    await user.click(screen.getByRole('button', { name: /login/i }));

    expect(await screen.findByText('Email is required')).toBeInTheDocument();
    expect(screen.getByText('Password is required')).toBeInTheDocument();
  });

  it('shows error for invalid email', async () => {
    render(<LoginForm onSubmit={vi.fn()} />);

    await user.type(screen.getByLabelText(/email/i), 'not-an-email');
    await user.click(screen.getByRole('button', { name: /login/i }));

    expect(await screen.findByText('Invalid email format')).toBeInTheDocument();
  });

  it('calls onSubmit with correct data when valid', async () => {
    const handleSubmit = vi.fn().mockResolvedValue(undefined);
    render(<LoginForm onSubmit={handleSubmit} />);

    await user.type(screen.getByLabelText(/email/i), 'user@example.com');
    await user.type(screen.getByLabelText(/password/i), 'securepass123');
    await user.click(screen.getByRole('button', { name: /login/i }));

    await waitFor(() => {
      expect(handleSubmit).toHaveBeenCalledWith({
        email: 'user@example.com',
        password: 'securepass123',
      });
    });
  });
});

Testing Async Components

// UserProfile.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import { UserProfile } from './UserProfile';

// Mock API với MSW (Mock Service Worker)
const server = setupServer(
  http.get('/api/users/:id', ({ params }) => {
    if (params.id === '1') {
      return HttpResponse.json({
        id: 1,
        name: 'Alice Johnson',
        email: 'alice@example.com',
      });
    }
    return new HttpResponse(null, { status: 404 });
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

describe('UserProfile', () => {
  it('shows loading state initially', () => {
    render(<UserProfile userId={1} />);
    expect(screen.getByRole('progressbar')).toBeInTheDocument();
    // hoặc
    expect(screen.getByText(/loading/i)).toBeInTheDocument();
  });

  it('displays user data after loading', async () => {
    render(<UserProfile userId={1} />);

    expect(await screen.findByText('Alice Johnson')).toBeInTheDocument();
    expect(screen.getByText('alice@example.com')).toBeInTheDocument();
  });

  it('shows error message when user not found', async () => {
    render(<UserProfile userId={999} />);

    expect(await screen.findByRole('alert')).toBeInTheDocument();
    expect(screen.getByText(/not found|error/i)).toBeInTheDocument();
  });

  it('retries on server error', async () => {
    server.use(
      http.get('/api/users/1', () =>
        new HttpResponse(null, { status: 500 })
      )
    );

    render(<UserProfile userId={1} />);
    expect(await screen.findByText(/error/i)).toBeInTheDocument();
  });
});

Testing Custom Hooks

// useCounter.test.ts
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';

describe('useCounter', () => {
  it('initializes with default value', () => {
    const { result } = renderHook(() => useCounter());
    expect(result.current.count).toBe(0);
  });

  it('initializes with provided value', () => {
    const { result } = renderHook(() => useCounter(5));
    expect(result.current.count).toBe(5);
  });

  it('increments count', () => {
    const { result } = renderHook(() => useCounter());

    act(() => {
      result.current.increment();
    });

    expect(result.current.count).toBe(1);
  });

  it('resets count', () => {
    const { result } = renderHook(() => useCounter(10));

    act(() => {
      result.current.reset();
    });

    expect(result.current.count).toBe(0);
  });
});

// useLocalStorage.test.ts
describe('useLocalStorage', () => {
  beforeEach(() => {
    localStorage.clear();
  });

  it('returns initial value when no stored value', () => {
    const { result } = renderHook(() => useLocalStorage('key', 'default'));
    expect(result.current[0]).toBe('default');
  });

  it('persists value to localStorage', () => {
    const { result } = renderHook(() => useLocalStorage('key', ''));

    act(() => {
      result.current[1]('new value');
    });

    expect(localStorage.getItem('key')).toBe('"new value"');
    expect(result.current[0]).toBe('new value');
  });
});

Testing với Context

// Tạo custom render wrapper
import { render } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { MemoryRouter } from 'react-router-dom';
import { AuthProvider } from './contexts/AuthContext';

function createTestQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        retry: false,     // Không retry trong test
        gcTime: 0,
      },
    },
  });
}

interface RenderOptions {
  initialRoute?: string;
  initialUser?: User;
}

function renderWithProviders(
  ui: React.ReactElement,
  { initialRoute = '/', initialUser }: RenderOptions = {}
) {
  const queryClient = createTestQueryClient();

  function Wrapper({ children }: { children: React.ReactNode }) {
    return (
      <QueryClientProvider client={queryClient}>
        <AuthProvider initialUser={initialUser}>
          <MemoryRouter initialEntries={[initialRoute]}>
            {children}
          </MemoryRouter>
        </AuthProvider>
      </QueryClientProvider>
    );
  }

  return render(ui, { wrapper: Wrapper });
}

// Test với context
describe('ProtectedRoute', () => {
  it('redirects to login when not authenticated', () => {
    renderWithProviders(<ProtectedPage />, { initialRoute: '/dashboard' });
    expect(screen.getByText(/login/i)).toBeInTheDocument();
  });

  it('renders content when authenticated', () => {
    renderWithProviders(<ProtectedPage />, {
      initialUser: { id: 1, name: 'Alice', roles: ['user'] },
    });
    expect(screen.getByText(/dashboard/i)).toBeInTheDocument();
  });
});

React Patterns

Higher-Order Components (HOC)

HOC là function nhận một component và trả về component mới với behavior được thêm vào.

// withLoading HOC
function withLoading<T extends object>(
  WrappedComponent: React.ComponentType<T>
) {
  return function WithLoadingComponent({
    isLoading,
    ...props
  }: T & { isLoading: boolean }) {
    if (isLoading) {
      return <div className="spinner">Loading...</div>;
    }
    return <WrappedComponent {...(props as T)} />;
  };
}

// withAuth HOC
function withAuth<T extends object>(WrappedComponent: React.ComponentType<T>) {
  return function WithAuthComponent(props: T) {
    const { isAuthenticated } = useAuth();

    if (!isAuthenticated) {
      return <Navigate to="/login" />;
    }

    return <WrappedComponent {...props} />;
  };
}

// withErrorBoundary HOC
function withErrorBoundary<T extends object>(
  WrappedComponent: React.ComponentType<T>,
  fallback: React.ReactNode
) {
  return function WithErrorBoundary(props: T) {
    return (
      <ErrorBoundary fallback={fallback}>
        <WrappedComponent {...props} />
      </ErrorBoundary>
    );
  };
}

// Kết hợp nhiều HOCs
const ProtectedDashboard = withAuth(withLoading(Dashboard));

// Sử dụng
<ProtectedDashboard isLoading={loading} data={data} />

Compound Components

Pattern cho phép components chia sẻ state ngầm, API linh hoạt như HTML (select/option).

import { createContext, useContext, useState } from 'react';

// Tabs compound component
interface TabsContextType {
  activeTab: string;
  setActiveTab: (id: string) => void;
}

const TabsContext = createContext<TabsContextType | null>(null);

function useTabs() {
  const ctx = useContext(TabsContext);
  if (!ctx) throw new Error('Must be used within Tabs');
  return ctx;
}

// Parent component giữ state
function Tabs({
  defaultTab,
  children,
}: {
  defaultTab: string;
  children: React.ReactNode;
}) {
  const [activeTab, setActiveTab] = useState(defaultTab);

  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      <div className="tabs">{children}</div>
    </TabsContext.Provider>
  );
}

// Sub-components
function TabList({ children }: { children: React.ReactNode }) {
  return <div className="tab-list" role="tablist">{children}</div>;
}

function Tab({ id, children }: { id: string; children: React.ReactNode }) {
  const { activeTab, setActiveTab } = useTabs();

  return (
    <button
      role="tab"
      aria-selected={activeTab === id}
      className={activeTab === id ? 'tab active' : 'tab'}
      onClick={() => setActiveTab(id)}
    >
      {children}
    </button>
  );
}

function TabPanels({ children }: { children: React.ReactNode }) {
  return <div className="tab-panels">{children}</div>;
}

function TabPanel({ id, children }: { id: string; children: React.ReactNode }) {
  const { activeTab } = useTabs();

  if (activeTab !== id) return null;

  return (
    <div role="tabpanel" className="tab-panel">
      {children}
    </div>
  );
}

// Gắn vào Tabs
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panels = TabPanels;
Tabs.Panel = TabPanel;

// Sử dụng - API rất tự nhiên
function App() {
  return (
    <Tabs defaultTab="overview">
      <Tabs.List>
        <Tabs.Tab id="overview">Overview</Tabs.Tab>
        <Tabs.Tab id="analytics">Analytics</Tabs.Tab>
        <Tabs.Tab id="settings">Settings</Tabs.Tab>
      </Tabs.List>
      <Tabs.Panels>
        <Tabs.Panel id="overview"><Overview /></Tabs.Panel>
        <Tabs.Panel id="analytics"><Analytics /></Tabs.Panel>
        <Tabs.Panel id="settings"><Settings /></Tabs.Panel>
      </Tabs.Panels>
    </Tabs>
  );
}

Portals

Render component vào DOM node bên ngoài component tree.

import { createPortal } from 'react-dom';
import { useEffect, useRef } from 'react';

// Modal với Portal
function Modal({
  isOpen,
  onClose,
  title,
  children,
}: {
  isOpen: boolean;
  onClose: () => void;
  title: string;
  children: React.ReactNode;
}) {
  const overlayRef = useRef<HTMLDivElement>(null);

  // Close khi click overlay
  function handleOverlayClick(e: React.MouseEvent) {
    if (e.target === overlayRef.current) {
      onClose();
    }
  }

  // Close khi nhấn Escape
  useEffect(() => {
    function handleKeyDown(e: KeyboardEvent) {
      if (e.key === 'Escape') onClose();
    }

    if (isOpen) {
      document.addEventListener('keydown', handleKeyDown);
      document.body.style.overflow = 'hidden'; // Ngăn scroll
    }

    return () => {
      document.removeEventListener('keydown', handleKeyDown);
      document.body.style.overflow = '';
    };
  }, [isOpen, onClose]);

  if (!isOpen) return null;

  return createPortal(
    <div
      ref={overlayRef}
      className="modal-overlay"
      onClick={handleOverlayClick}
      role="dialog"
      aria-modal="true"
      aria-labelledby="modal-title"
    >
      <div className="modal-content">
        <div className="modal-header">
          <h2 id="modal-title">{title}</h2>
          <button onClick={onClose} aria-label="Close modal">×</button>
        </div>
        <div className="modal-body">{children}</div>
      </div>
    </div>,
    document.body // Render trực tiếp vào body
  );
}

// Sử dụng
function App() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setIsOpen(true)}>Open Modal</button>
      <Modal isOpen={isOpen} onClose={() => setIsOpen(false)} title="Confirm">
        <p>Are you sure?</p>
        <button onClick={() => setIsOpen(false)}>Cancel</button>
        <button onClick={handleConfirm}>Confirm</button>
      </Modal>
    </div>
  );
}

Error Boundaries

Bắt JavaScript errors trong component tree, display fallback UI.

// ErrorBoundary phải là Class Component
import { Component, ErrorInfo, ReactNode } from 'react';

interface Props {
  fallback: ReactNode | ((error: Error) => ReactNode);
  onError?: (error: Error, info: ErrorInfo) => void;
  children: ReactNode;
}

interface State {
  hasError: boolean;
  error: Error | null;
}

class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, info: ErrorInfo) {
    this.props.onError?.(error, info);
    // Log to error reporting service
    console.error('Error boundary caught:', error, info);
  }

  render() {
    if (this.state.hasError && this.state.error) {
      const { fallback } = this.props;
      return typeof fallback === 'function'
        ? fallback(this.state.error)
        : fallback;
    }

    return this.props.children;
  }
}

// Sử dụng
function App() {
  return (
    <ErrorBoundary
      fallback={(error) => (
        <div className="error-page">
          <h1>Something went wrong</h1>
          <p>{error.message}</p>
          <button onClick={() => window.location.reload()}>Reload</button>
        </div>
      )}
      onError={(error) => logErrorToService(error)}
    >
      <Dashboard />
    </ErrorBoundary>
  );
}

Render Props Pattern

// Mouse tracker với render props
function MouseTracker({
  render,
}: {
  render: (pos: { x: number; y: number }) => ReactNode;
}) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  return (
    <div
      onMouseMove={(e) => setPosition({ x: e.clientX, y: e.clientY })}
      style={{ height: '300px', border: '1px solid #ccc' }}
    >
      {render(position)}
    </div>
  );
}

// Sử dụng
<MouseTracker
  render={({ x, y }) => (
    <p>
      Mouse position: ({x}, {y})
    </p>
  )}
/>

Lưu ý: Ngày nay Custom Hooks thường thay thế Render Props và HOCs vì code gọn hơn.

Next.js Cơ bản

Giới thiệu

Next.js là React framework cho production, cung cấp SSR, SSG, file-based routing, và nhiều tính năng khác out-of-the-box.

npx create-next-app@latest my-app --typescript --tailwind --app

App Router (Next.js 13+)

Cấu trúc thư mục app/ với file-based routing.

app/
├── layout.tsx          # Root layout (wrap tất cả pages)
├── page.tsx            # Route: /
├── loading.tsx         # Loading UI
├── error.tsx           # Error UI
├── not-found.tsx       # 404 page
├── (marketing)/        # Route group (không ảnh hưởng URL)
│   ├── about/
│   │   └── page.tsx    # Route: /about
│   └── contact/
│       └── page.tsx    # Route: /contact
├── blog/
│   ├── page.tsx        # Route: /blog
│   └── [slug]/
│       └── page.tsx    # Route: /blog/:slug
└── api/
    └── users/
        └── route.ts    # API Route: GET/POST /api/users
// app/layout.tsx - Root Layout
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';

const inter = Inter({ subsets: ['latin'] });

export const metadata: Metadata = {
  title: 'My App',
  description: 'Built with Next.js',
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="vi">
      <body className={inter.className}>
        <Header />
        <main>{children}</main>
        <Footer />
      </body>
    </html>
  );
}

Server Components vs Client Components

// Server Component (default) - chạy trên server
// Có thể async, trực tiếp fetch data, không dùng hooks

// app/products/page.tsx
async function ProductsPage() {
  // Fetch data trực tiếp - không cần useEffect
  const products = await fetch('https://api.example.com/products', {
    next: { revalidate: 3600 }, // Cache 1 giờ
  }).then((r) => r.json());

  return (
    <div>
      <h1>Products</h1>
      {products.map((p) => (
        <ProductCard key={p.id} product={p} />
      ))}
    </div>
  );
}

export default ProductsPage;
// Client Component - cần 'use client' directive
'use client';

import { useState } from 'react';

function AddToCartButton({ productId }: { productId: number }) {
  const [added, setAdded] = useState(false);

  async function handleClick() {
    await addToCart(productId);
    setAdded(true);
  }

  return (
    <button onClick={handleClick}>
      {added ? '✓ Added' : 'Add to Cart'}
    </button>
  );
}
Quy tắc:
- Server Component: Fetch data, Database access, File system, Secret keys
- Client Component: Event listeners, useState/useEffect, Browser APIs

Tối ưu: Push "use client" xuống leaves của component tree
ServerComponent → ServerComponent → ClientComponent (leaf)

Data Fetching Patterns

// 1. Static (SSG) - build time, tốt cho SEO
async function BlogPage() {
  const posts = await fetchPosts();
  return <PostList posts={posts} />;
}

export const dynamic = 'force-static'; // Luôn static

// 2. Dynamic (SSR) - mỗi request
async function DashboardPage() {
  const data = await fetchUserData();
  return <Dashboard data={data} />;
}

export const dynamic = 'force-dynamic';

// 3. Incremental Static Regeneration (ISR)
async function ProductPage({ params }: { params: { id: string } }) {
  const product = await fetchProduct(params.id);
  return <ProductDetail product={product} />;
}

export const revalidate = 60; // Regenerate sau 60 giây

// 4. Parallel data fetching
async function UserDashboard({ userId }: { params: { userId: string } }) {
  // Fetch song song - không await tuần tự
  const [user, orders, notifications] = await Promise.all([
    fetchUser(userId),
    fetchOrders(userId),
    fetchNotifications(userId),
  ]);

  return (
    <div>
      <UserProfile user={user} />
      <OrderHistory orders={orders} />
      <Notifications items={notifications} />
    </div>
  );
}

Dynamic Routes

// app/blog/[slug]/page.tsx
interface Props {
  params: { slug: string };
  searchParams: { preview?: string };
}

// Generate static paths (SSG)
export async function generateStaticParams() {
  const posts = await fetchAllPosts();
  return posts.map((post) => ({ slug: post.slug }));
}

// Generate metadata dynamically
export async function generateMetadata({ params }: Props) {
  const post = await fetchPost(params.slug);
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      images: [post.coverImage],
    },
  };
}

async function BlogPost({ params, searchParams }: Props) {
  const post = await fetchPost(params.slug);

  if (!post) {
    notFound(); // Trigger not-found.tsx
  }

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

export default BlogPost;

API Routes

// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const page = Number(searchParams.get('page') ?? '1');

  const users = await db.user.findMany({
    skip: (page - 1) * 10,
    take: 10,
  });

  return NextResponse.json({ users, page });
}

export async function POST(request: NextRequest) {
  const body = await request.json();

  // Validate
  const result = createUserSchema.safeParse(body);
  if (!result.success) {
    return NextResponse.json(
      { error: result.error.format() },
      { status: 400 }
    );
  }

  const user = await db.user.create({ data: result.data });
  return NextResponse.json(user, { status: 201 });
}

// app/api/users/[id]/route.ts
export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const user = await db.user.findUnique({
    where: { id: Number(params.id) },
  });

  if (!user) {
    return NextResponse.json({ error: 'Not found' }, { status: 404 });
  }

  return NextResponse.json(user);
}

Middleware

// middleware.ts (ở root)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const token = request.cookies.get('auth-token')?.value;
  const isAuthPage = request.nextUrl.pathname.startsWith('/auth');
  const isProtectedRoute = request.nextUrl.pathname.startsWith('/dashboard');

  // Redirect unauthenticated users
  if (isProtectedRoute && !token) {
    return NextResponse.redirect(new URL('/auth/login', request.url));
  }

  // Redirect authenticated users away from auth pages
  if (isAuthPage && token) {
    return NextResponse.redirect(new URL('/dashboard', request.url));
  }

  // Add custom header
  const response = NextResponse.next();
  response.headers.set('X-Request-ID', crypto.randomUUID());
  return response;
}

export const config = {
  matcher: ['/dashboard/:path*', '/auth/:path*'],
};

So sánh Rendering Strategies

StrategyKhi nàoVí dụ
SSG (Static)Content ít thay đổiBlog, Landing page
ISRContent thay đổi vừaProduct page, News
SSRData real-time, user-specificDashboard, Checkout
CSRInteractive, no SEO neededAdmin panel widgets
StreamingNhiều data sourcesComplex dashboard

Elasticsearch

Giới thiệu

Elasticsearch là search engine phân tán, được xây dựng trên Apache Lucene. Nó cung cấp khả năng tìm kiếm full-text, phân tích log, và lưu trữ dữ liệu ở dạng JSON document.

┌──────────────────────────────────────────────────────────────┐
│                    Elasticsearch Cluster                     │
│                                                              │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐         │
│  │   Node 1    │  │   Node 2    │  │   Node 3    │         │
│  │  (Master)   │  │  (Data)     │  │  (Data)     │         │
│  │             │  │             │  │             │         │
│  │  Shard 1P   │  │  Shard 1R   │  │  Shard 2P   │         │
│  │  Shard 2R   │  │  Shard 2P   │  │  Shard 1R   │         │
│  └─────────────┘  └─────────────┘  └─────────────┘         │
│                                                              │
│  P = Primary Shard,  R = Replica Shard                      │
└──────────────────────────────────────────────────────────────┘

Stack

Tài liệu này sử dụng Elastic.Clients.Elasticsearch - .NET client chính thức cho Elasticsearch 8+.

dotnet add package Elastic.Clients.Elasticsearch

Mục lục

#Chủ đềMô tả
1Concepts Cơ bản & Kết nốiIndex, Shard, Replica, DI setup trong ASP.NET Core
2Mapping & Field TypesAttribute mapping, Fluent mapping, text vs keyword, nested
3Indexing DocumentsCRUD, Bulk API, Upsert, Ingest Pipeline qua .NET
4Basic SearchMatch, Term, Range, Bool query, Sorting qua .NET
5Query DSL Nâng caoMulti-match, Fuzzy, Nested, Highlight, Scroll
6AggregationsMetric, Terms, Range, Date Histogram, Faceted Search
7Analyzers & TokenizersBuilt-in analyzers, Custom analyzer, Autocomplete
8Performance TuningFilter vs must, Source filtering, Search After, ILM
9Cluster ManagementHealth check, ILM, Alias, Snapshot, ASP.NET Core integration

Use Cases

Use CasePhù hợp
Full-text search✅ Mạnh nhất
Log analytics (ELK Stack)✅ Phổ biến
Application search
Geospatial search
Real-time analytics
Primary database❌ Không phù hợp
Transactional data❌ Không phù hợp

Concepts Cơ bản & Kết nối .NET

Cài đặt

# Package chính thức cho Elasticsearch 8+
dotnet add package Elastic.Clients.Elasticsearch

# Nếu dùng Elasticsearch 7 (NEST - legacy)
dotnet add package NEST

Kết nối trong ASP.NET Core

// appsettings.json
{
  "Elasticsearch": {
    "Uri": "https://localhost:9200",
    "Username": "elastic",
    "Password": "changeme",
    "DefaultIndex": "products"
  }
}
// Program.cs
using Elastic.Clients.Elasticsearch;
using Elastic.Transport;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<ElasticsearchClient>(sp =>
{
    var config = builder.Configuration.GetSection("Elasticsearch");
    var uri = new Uri(config["Uri"]!);

    var settings = new ElasticsearchClientSettings(uri)
        .Authentication(new BasicAuthentication(config["Username"]!, config["Password"]!))
        .DefaultIndex(config["DefaultIndex"]!)
        // Map C# type sang index name
        .DefaultMappingFor<Product>(m => m.IndexName("products"))
        .DefaultMappingFor<Order>(m => m.IndexName("orders"))
        .EnableDebugMode()           // Bật khi develop - log requests
        .DisableDirectStreaming();   // Bật khi debug - read response body

    return new ElasticsearchClient(settings);
});

// Kiểm tra kết nối
var app = builder.Build();
var es = app.Services.GetRequiredService<ElasticsearchClient>();
var ping = await es.PingAsync();
if (!ping.IsSuccess()) throw new Exception("Cannot connect to Elasticsearch");

Index, Document, Shard

Bảng so sánh với SQL:

Elasticsearch          SQL
─────────────         ──────────────
Index            ≈    Table
Document         ≈    Row
Field            ≈    Column
Mapping          ≈    Schema

Lưu ý: Không có khái niệm "Database" cấp cao hơn như SQL.
Elasticsearch → Index → Document

Document

Đơn vị dữ liệu cơ bản, lưu dạng JSON.

// Một document trong index "products"
{
  "_index": "products",
  "_id": "1",
  "_version": 1,
  "_source": {
    "name": "iPhone 15 Pro",
    "brand": "Apple",
    "price": 999.99,
    "category": "smartphones",
    "tags": ["5G", "flagship", "camera"],
    "specs": {
      "storage": "256GB",
      "ram": "8GB",
      "screen": "6.1 inch"
    },
    "in_stock": true,
    "created_at": "2024-01-15T10:30:00Z"
  }
}

Index

Tập hợp các documents có cấu trúc tương tự.

# Tạo index
PUT /products
{
  "settings": {
    "number_of_shards": 3,     # Số primary shards
    "number_of_replicas": 1    # Số replica per primary
  },
  "mappings": {
    "properties": {
      "name": { "type": "text" },
      "price": { "type": "float" }
    }
  }
}

# Xem thông tin index
GET /products

# Xóa index
DELETE /products

# Liệt kê tất cả indices
GET /_cat/indices?v

Shards & Replicas

┌──────────────────────────────────────────────────────────┐
│              Index "products" (1000 documents)           │
│                                                          │
│  Primary Shard 1    Primary Shard 2    Primary Shard 3  │
│  (documents 1-333)  (documents 334-666) (667-1000)      │
│         │                  │                  │         │
│         ▼                  ▼                  ▼         │
│  Replica Shard 1    Replica Shard 2    Replica Shard 3  │
│  (backup của P1)    (backup của P2)    (backup của P3)  │
└──────────────────────────────────────────────────────────┘

Primary Shard:

  • Số lượng cố định sau khi tạo index (không thể thay đổi)
  • Dữ liệu được phân tán đều qua các primary shards
  • Default: 1 shard (ES 7+), trước đây là 5

Replica Shard:

  • Bản sao của primary shard
  • Có thể thay đổi số replica bất kỳ lúc nào
  • Tăng read throughput, không tăng write throughput
  • Không đặt trên cùng node với primary của nó
# Thay đổi số replicas (có thể thay đổi sau khi tạo)
PUT /products/_settings
{
  "number_of_replicas": 2
}

Chọn số Shards phù hợp

Nguyên tắc:
- Mỗi shard ≈ 10-50GB data
- Số shards ≈ (tổng data / 30GB) hoặc số nodes
- Quá nhiều shards → overhead, slow
- Quá ít shards → không scale được

Ví dụ:
- 30GB data → 1 primary shard
- 300GB data → 10 primary shards
- 3TB data với 10 nodes → 100 primary shards

Cluster & Nodes

# Xem health của cluster
GET /_cluster/health

# Response
{
  "cluster_name": "my-cluster",
  "status": "green",         # green/yellow/red
  "number_of_nodes": 3,
  "number_of_data_nodes": 3,
  "active_primary_shards": 10,
  "active_shards": 20,
  "relocating_shards": 0,
  "initializing_shards": 0,
  "unassigned_shards": 0
}

Cluster Status:

  • Green: Tất cả primary và replica shards đều active
  • Yellow: Tất cả primary active nhưng một số replica chưa được assign
  • Red: Một số primary shards không hoạt động

Node Types:

Master Node: Quản lý cluster (shards, indices)
Data Node: Lưu trữ data, thực hiện CRUD và search
Ingest Node: Pre-processing trước khi index (pipeline)
Coordinating Node: Route requests, merge results

Inverted Index

Cơ chế tìm kiếm full-text nhanh của Elasticsearch.

Documents:
Doc 1: "The quick brown fox"
Doc 2: "The lazy brown dog"
Doc 3: "The fox ate the dog"

Inverted Index:
┌──────────┬──────────────────┐
│  Term    │  Documents       │
├──────────┼──────────────────┤
│  the     │  [1, 2, 3]       │
│  quick   │  [1]             │
│  brown   │  [1, 2]          │
│  fox     │  [1, 3]          │
│  lazy    │  [2]             │
│  dog     │  [2, 3]          │
│  ate     │  [3]             │
└──────────┴──────────────────┘

Query: "fox"
→ Look up inverted index → Documents [1, 3]
→ Rất nhanh! O(1) lookup

Near Real-Time (NRT)

Document được index → Lưu vào buffer (in-memory)
                    → Sau mỗi 1 giây (refresh): buffer → segment
                    → Segment có thể search (NRT)
                    → Sau mỗi 30 phút (flush): segment → disk

Mặc định: Có thể search sau ~1 giây
# Force refresh ngay lập tức (tốn CPU)
POST /products/_refresh

# Thay đổi refresh interval
PUT /products/_settings
{
  "refresh_interval": "30s"   # Tăng để tăng tốc indexing
  # "refresh_interval": "-1"  # Tắt auto-refresh hoàn toàn
}

Document Versioning

# Mỗi document có version number
PUT /products/_doc/1
{ "name": "iPhone 15" }
# Response: "_version": 1

PUT /products/_doc/1
{ "name": "iPhone 15 Pro" }
# Response: "_version": 2

# Optimistic concurrency control
PUT /products/_doc/1?if_seq_no=2&if_primary_term=1
{ "name": "Updated name" }
# Fails nếu document đã được update bởi người khác

Mapping & Field Types với .NET

Cách tiếp cận Mapping trong .NET

Có 3 cách khai báo mapping khi dùng Elastic.Clients.Elasticsearch:

1. Attribute Mapping   - Khai báo trên model class (đơn giản, tập trung)
2. Fluent Mapping      - Trong code khi tạo index (linh hoạt, type-safe)
3. Inference Mapping   - ES client tự suy ra từ .NET types (nhanh nhưng ít kiểm soát)

Attribute Mapping

using Elastic.Clients.Elasticsearch.Mapping;

public class Product
{
    // Không cần attribute - ES tự map theo type name
    public int Id { get; set; }

    // Text field: full-text search (analyzed)
    [Text(Analyzer = "standard")]
    public string Name { get; set; } = string.Empty;

    // Text với custom analyzer
    [Text(Analyzer = "english", SearchAnalyzer = "english")]
    public string? Description { get; set; }

    // Keyword: exact match, sort, aggregation
    [Keyword(IgnoreAbove = 256)]
    public string Category { get; set; } = string.Empty;

    // ScaledFloat: lưu như long, tốt cho tiền tệ
    [ScaledFloat(ScalingFactor = 100)]
    public decimal Price { get; set; }

    // Numeric
    [Integer]
    public int StockQuantity { get; set; }

    [Boolean]
    public bool InStock { get; set; }

    // Array - không cần type đặc biệt trong ES
    [Keyword]
    public List<string> Tags { get; set; } = [];

    [Date(Format = "strict_date_optional_time")]
    public DateTime CreatedAt { get; set; }

    // Object thông thường (flatten trong ES)
    public ProductSpecs? Specs { get; set; }
}

public class ProductSpecs
{
    [Keyword]
    public string? Storage { get; set; }

    [Keyword]
    public string? Ram { get; set; }

    [HalfFloat]
    public float? ScreenSize { get; set; }
}

Fluent Mapping trong CreateIndex

public async Task CreateProductIndexAsync()
{
    await _es.Indices.CreateAsync<Product>("products", c => c
        .Settings(s => s
            .NumberOfShards(1)
            .NumberOfReplicas(1)
            .Analysis(a => a
                .Analyzers(an => an
                    .Custom("vi_analyzer", ca => ca
                        .Tokenizer("standard")
                        .Filter(["lowercase", "asciifolding"])
                    )
                )
            )
        )
        .Mappings(m => m
            .Dynamic(DynamicMapping.Strict) // Chỉ cho phép fields đã khai báo
            .Properties(p => p
                // text + keyword multi-field (search and sort on same field)
                .Text(t => t
                    .Name(n => n.Name)
                    .Analyzer("vi_analyzer")
                    .Fields(f => f
                        .Keyword(k => k
                            .Name("keyword")
                            .IgnoreAbove(256)
                        )
                    )
                )
                .Text(t => t.Name(n => n.Description).Analyzer("english"))
                .Keyword(k => k.Name(n => n.Category))
                .Keyword(k => k.Name(n => n.Brand))
                .ScaledFloat(sf => sf.Name(n => n.Price).ScalingFactor(100))
                .Boolean(b => b.Name(n => n.InStock))
                .Integer(i => i.Name(n => n.StockQuantity))
                .Keyword(k => k.Name(n => n.Tags))
                .Date(d => d.Name(n => n.CreatedAt).Format("strict_date_optional_time"))
                .GeoPoint(g => g.Name("location"))
                // Nested object - giữ quan hệ giữa sub-fields
                .Nested<ProductReview>(n => n
                    .Name("reviews")
                    .Properties(rp => rp
                        .Keyword(k => k.Name(r => r.UserId))
                        .Byte(b => b.Name(r => r.Score))
                        .Text(t => t.Name(r => r.Comment))
                    )
                )
            )
        )
    );
}

Field Types Quan trọng

text vs keyword

// text: Full-text search - được phân tích (tokenize, lowercase, v.v.)
// → "Apple iPhone 15 Pro" → tokens: ["apple", "iphone", "15", "pro"]
// → Tìm "iphone" → Match ✅

// keyword: Exact match - không phân tích
// → "Apple iPhone 15 Pro" → lưu nguyên
// → Tìm "iphone" → No match ❌
// → Dùng cho: filter chính xác, sort, aggregation

// Multi-field: dùng cả hai cho cùng một field
.Text(t => t.Name(n => n.Category)
    .Fields(f => f.Keyword(k => k.Name("keyword")))
)
// Search full-text: category
// Aggregation / Sort: category.keyword

Nested vs Object

// Object (mặc định): flatten → mất quan hệ trong array
// Vấn đề:
// reviews: [{ user: "Alice", score: 5 }, { user: "Bob", score: 1 }]
// Flatten: reviews.user: ["Alice", "Bob"], reviews.score: [5, 1]
// → Query user=Alice AND score=1 → Sai! (match vì flatten)

// Nested: mỗi item là hidden document riêng
.Nested<ProductReview>(n => n
    .Name("reviews")
    .Properties(p => p
        .Keyword(k => k.Name(r => r.UserId))
        .Byte(b => b.Name(r => r.Score))
    )
)
// Dùng nested query để tìm chính xác theo cặp

Xem Mapping từ .NET

public async Task<string> GetMappingAsync()
{
    var response = await _es.Indices.GetMappingAsync(m =>
        m.Indices("products")
    );

    var properties = response.Indices["products"].Mappings.Properties;
    foreach (var (name, prop) in properties)
    {
        Console.WriteLine($"{name}: {prop.Type}");
    }

    return response.DebugInformation;
}

Thay đổi Mapping - Reindex Pattern

// Không thể thay đổi field type sau khi đã có data → cần Reindex

public async Task ReindexAsync(string sourceIndex, string destIndex)
{
    // 1. Tạo index mới với mapping đúng
    await CreateIndexWithNewMappingAsync(destIndex);

    // 2. Reindex data
    var response = await _es.ReindexAsync(r => r
        .Source(s => s.Index(sourceIndex))
        .Dest(d => d.Index(destIndex))
    );

    if (!response.IsSuccess())
        throw new Exception($"Reindex failed: {response.DebugInformation}");

    Console.WriteLine($"Reindexed {response.Total} documents");

    // 3. Chuyển alias (zero-downtime)
    await _es.Indices.UpdateAliasesAsync(a => a
        .Actions(
            new RemoveIndexAction { Index = sourceIndex, Alias = "products-live" },
            new AddAction { Index = destIndex, Alias = "products-live" }
        )
    );
}

Dynamic Mapping

Elasticsearch tự động phát hiện và tạo mapping khi index document lần đầu.

# Index một document mà không cần tạo mapping trước (tham khảo)
POST /products/_doc
{
  "name": "Laptop Pro",
  "price": 1299.99,
  "in_stock": true,
  "tags": ["laptop", "business"],
  "released_at": "2024-01-15"
}

# ES tự động tạo mapping
GET /products/_mapping
# Response:
{
  "products": {
    "mappings": {
      "properties": {
        "name":        { "type": "text", "fields": { "keyword": { "type": "keyword" } } },
        "price":       { "type": "float" },
        "in_stock":    { "type": "boolean" },
        "tags":        { "type": "text", "fields": { "keyword": { "type": "keyword" } } },
        "released_at": { "type": "date" }
      }
    }
  }
}

Vấn đề với Dynamic Mapping:

  • Có thể tạo mapping không mong muốn
  • Không thể thay đổi field type sau khi đã có data
  • Có thể gây “mapping explosion” với data dynamic

Explicit Mapping - Best Practice

# Tạo index với mapping rõ ràng TRƯỚC khi index data
PUT /products
{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 1
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "standard",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      },
      "description": {
        "type": "text",
        "analyzer": "english"
      },
      "price": {
        "type": "scaled_float",
        "scaling_factor": 100
      },
      "category": {
        "type": "keyword"
      },
      "tags": {
        "type": "keyword"
      },
      "brand": {
        "type": "keyword"
      },
      "rating": {
        "type": "half_float"
      },
      "in_stock": {
        "type": "boolean"
      },
      "stock_quantity": {
        "type": "integer"
      },
      "created_at": {
        "type": "date",
        "format": "strict_date_optional_time||epoch_millis"
      },
      "location": {
        "type": "geo_point"
      },
      "specs": {
        "type": "object",
        "properties": {
          "storage": { "type": "keyword" },
          "ram": { "type": "keyword" },
          "screen_size": { "type": "float" }
        }
      },
      "reviews": {
        "type": "nested",
        "properties": {
          "user_id": { "type": "keyword" },
          "score": { "type": "byte" },
          "comment": { "type": "text" }
        }
      }
    }
  }
}

Field Types Quan trọng

text vs keyword

# text: Full-text search, được analyze (tách từ, lowercase, v.v.)
# keyword: Exact match, sorting, aggregations, không analyze

# Ví dụ:
"name": "Apple iPhone 15 Pro"

# text field → tokens: ["apple", "iphone", "15", "pro"]
# → Tìm "iphone" → Match!
# → Tìm "Apple iPhone 15 Pro" → Match!

# keyword field → lưu nguyên "Apple iPhone 15 Pro"
# → Tìm "iphone" → No match!
# → Tìm "Apple iPhone 15 Pro" → Match!
# → Dùng cho filter, sort, aggregations
# Multi-field: vừa search full-text vừa filter/sort
"name": {
  "type": "text",
  "fields": {
    "keyword": { "type": "keyword" }  # name.keyword
  }
}

# Search full-text
GET /products/_search
{ "query": { "match": { "name": "iphone" } } }

# Exact filter hoặc aggregation
GET /products/_search
{ "aggs": { "brands": { "terms": { "field": "name.keyword" } } } }

Numeric Types

byte:         -128 to 127
short:        -32,768 to 32,767
integer:      -2^31 to 2^31-1
long:         -2^63 to 2^63-1
float:        32-bit IEEE 754
double:       64-bit IEEE 754
half_float:   16-bit (tiết kiệm space, kém chính xác hơn)
scaled_float: Stored as long, scaled by factor (tốt cho currency)

# Dùng scaled_float cho giá tiền
"price": {
  "type": "scaled_float",
  "scaling_factor": 100  # 19.99 → stored as 1999
}

date

"created_at": {
  "type": "date",
  "format": "strict_date_optional_time||yyyy-MM-dd||epoch_millis"
}

# Tất cả formats đều được chấp nhận:
{ "created_at": "2024-01-15" }
{ "created_at": "2024-01-15T10:30:00Z" }
{ "created_at": 1705315800000 }   # Unix timestamp ms

nested vs object

# object: Thông thường, flatten thành flat fields
"address": {
  "type": "object",
  "properties": {
    "city": { "type": "keyword" },
    "country": { "type": "keyword" }
  }
}
# Vấn đề: Mất mối quan hệ giữa fields của các objects trong array

# Ví dụ vấn đề:
{
  "comments": [
    { "user": "Alice", "score": 5 },
    { "user": "Bob",   "score": 1 }
  ]
}
# Flatten: comments.user: ["Alice", "Bob"]
#          comments.score: [5, 1]
# Query: user=Alice AND score=1 → Sai! Match vì không theo cặp

# nested: Mỗi nested document là hidden document riêng
"comments": {
  "type": "nested",
  "properties": {
    "user":  { "type": "keyword" },
    "score": { "type": "integer" }
  }
}
# Query nested: Đúng! Dùng nested query

Index Templates

# Áp dụng settings/mappings tự động cho indices mới
PUT /_index_template/products-template
{
  "index_patterns": ["products-*"],   # Áp dụng với indices matching pattern
  "priority": 100,
  "template": {
    "settings": {
      "number_of_shards": 1,
      "number_of_replicas": 1
    },
    "mappings": {
      "dynamic": "strict",            # Không cho phép fields không có mapping
      "properties": {
        "name": { "type": "text" },
        "price": { "type": "scaled_float", "scaling_factor": 100 }
      }
    }
  }
}

Thay đổi Mapping

# Không thể thay đổi field type sau khi đã index data
# Giải pháp: Reindex

# 1. Tạo index mới với mapping đúng
PUT /products-v2
{ "mappings": { "properties": { ... } } }

# 2. Reindex data
POST /_reindex
{
  "source": { "index": "products" },
  "dest": { "index": "products-v2" }
}

# 3. Tạo alias trỏ tới index mới
POST /_aliases
{
  "actions": [
    { "remove": { "index": "products",    "alias": "products-latest" } },
    { "add":    { "index": "products-v2", "alias": "products-latest" } }
  ]
}

# Application dùng alias → không cần đổi code
GET /products-latest/_search

Indexing Documents với .NET

Model & DI Setup

// Models/Product.cs
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string? Description { get; set; }
    public string Category { get; set; } = string.Empty;
    public string Brand { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public bool InStock { get; set; }
    public int StockQuantity { get; set; }
    public List<string> Tags { get; set; } = [];
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}

// Program.cs - DI registration
builder.Services.AddSingleton<ElasticsearchClient>(_ =>
{
    var settings = new ElasticsearchClientSettings(new Uri("https://localhost:9200"))
        .Authentication(new BasicAuthentication("elastic", "changeme"))
        .DefaultMappingFor<Product>(m => m.IndexName("products"))
        .DefaultMappingFor<Order>(m => m.IndexName("orders"));

    return new ElasticsearchClient(settings);
});

Tạo Index với Mapping

public async Task CreateIndexAsync()
{
    var exists = await _es.Indices.ExistsAsync("products");
    if (exists.Exists) return;

    await _es.Indices.CreateAsync<Product>("products", c => c
        .Settings(s => s
            .NumberOfShards(1)
            .NumberOfReplicas(1)
        )
        .Mappings(m => m
            .Dynamic(DynamicMapping.Strict)
            .Properties(p => p
                .Text(t => t.Name(n => n.Name)
                    .Analyzer("standard")
                    .Fields(f => f.Keyword(k => k.Name("keyword")))
                )
                .Text(t => t.Name(n => n.Description).Analyzer("english"))
                .Keyword(k => k.Name(n => n.Category))
                .ScaledFloat(sf => sf.Name(n => n.Price).ScalingFactor(100))
                .Boolean(b => b.Name(n => n.InStock))
                .Integer(i => i.Name(n => n.StockQuantity))
                .Keyword(k => k.Name(n => n.Tags))
                .Date(d => d.Name(n => n.CreatedAt))
            )
        )
    );
}

CRUD Operations

Create / Index

// Index single document
public async Task<bool> IndexProductAsync(Product product)
{
    var response = await _es.IndexAsync(product, i => i
        .Index("products")
        .Id(product.Id.ToString())
    );
    return response.IsSuccess();
}

// Bulk index nhiều documents
public async Task BulkIndexAsync(IEnumerable<Product> products)
{
    var response = await _es.BulkAsync(b => b
        .Index("products")
        .IndexMany(products, (d, p) => d.Id(p.Id.ToString()))
    );

    if (response.Errors)
    {
        var failed = response.ItemsWithErrors.Select(i => i.Id);
        throw new Exception($"Bulk failed for IDs: {string.Join(", ", failed)}");
    }
}

Get

public async Task<Product?> GetByIdAsync(int id)
{
    var response = await _es.GetAsync<Product>(id.ToString());
    return response.Found ? response.Source : null;
}

// Multi-get
public async Task<List<Product>> GetManyAsync(IEnumerable<int> ids)
{
    var response = await _es.MgetAsync<Product>(m => m
        .Ids(ids.Select(i => (Id)i.ToString()))
    );

    return response.Docs
        .OfType<GetResponse<Product>>()
        .Where(d => d.Found && d.Source is not null)
        .Select(d => d.Source!)
        .ToList();
}

Update

// Partial update - chỉ fields cần thay
public async Task UpdatePriceAsync(int id, decimal newPrice)
{
    await _es.UpdateAsync<Product, object>(id.ToString(), u => u
        .Doc(new { Price = newPrice })
    );
}

// Update với script
public async Task IncrementStockAsync(int id, int amount)
{
    await _es.UpdateAsync<Product, object>(id.ToString(), u => u
        .Script(s => s
            .Source("ctx._source.stock_quantity += params.amount")
            .Params(p => p.Add("amount", amount))
        )
    );
}

// Upsert
public async Task UpsertProductAsync(Product product)
{
    await _es.UpdateAsync<Product, Product>(product.Id.ToString(), u => u
        .Doc(product)
        .DocAsUpsert(true)
    );
}

Delete

// Delete by ID
public async Task<bool> DeleteAsync(int id)
{
    var response = await _es.DeleteAsync(id.ToString(), d => d.Index("products"));
    return response.IsSuccess();
}

// Delete by query (e.g. out-of-stock products with price < 10)
public async Task DeleteByQueryAsync(decimal maxPrice)
{
    await _es.DeleteByQueryAsync<Product>(d => d
        .Query(q => q
            .Range(r => r
                .NumberRange(nr => nr
                    .Field(f => f.Price)
                    .LessThan((double)maxPrice)
                )
            )
        )
    );
}

Bulk Indexing - Pattern Hiệu suất cao

// Tắt refresh, bulk index, bật lại → nhanh hơn nhiều
public async Task FullReindexAsync(IAsyncEnumerable<Product> products)
{
    // 1. Tắt refresh
    await _es.Indices.PutSettingsAsync("products", s =>
        s.RefreshInterval(new Duration("-1"))
    );

    try
    {
        var batch = new List<Product>();

        await foreach (var product in products)
        {
            batch.Add(product);
            if (batch.Count >= 500)
            {
                await _es.BulkAsync(b => b
                    .Index("products")
                    .IndexMany(batch, (d, p) => d.Id(p.Id.ToString()))
                );
                batch.Clear();
            }
        }

        if (batch.Count > 0)
            await _es.BulkAsync(b => b
                .Index("products")
                .IndexMany(batch, (d, p) => d.Id(p.Id.ToString()))
            );
    }
    finally
    {
        // 2. Bật lại refresh
        await _es.Indices.PutSettingsAsync("products", s =>
            s.RefreshInterval(new Duration("1s"))
        );
        await _es.Indices.RefreshAsync("products");
    }
}

Equivalent JSON (tham khảo)

// PUT /products/_doc/1 (tương đương IndexAsync)
{
  "name": "iPhone 15 Pro",
  "price": 999.99,
  "category": "smartphones",
  "in_stock": true
}

// POST /products/_update/1 (tương đương UpdateAsync partial)
{
  "doc": { "price": 899.99 }
}

// POST /_bulk
{ "index": { "_index": "products", "_id": "1" } }
{ "name": "iPhone 15 Pro", "price": 999.99 }
{ "index": { "_index": "products", "_id": "2" } }
{ "name": "Galaxy S24", "price": 899.99 }

Lưu ý: Trong .NET client, DefaultMappingFor<T> đã gắn index name vào type, nên không cần chỉ định .Index(...) mỗi lần nếu đã config trong Program.cs.

CRUD Operations - Raw API

Create / Index

# PUT với ID cụ thể - tạo mới hoặc replace toàn bộ document (tham khảo thêm)
PUT /products/_doc/1
{
  "name": "iPhone 15 Pro",
  "price": 999.99,
  "category": "smartphones"
}

# POST không có ID - ES tự generate ID
POST /products/_doc
{
  "name": "Galaxy S24",
  "price": 899.99
}
# Response: "_id": "abcdef123456..."

# PUT _create - chỉ tạo mới, fail nếu ID đã tồn tại
PUT /products/_create/1
{
  "name": "iPhone 15 Pro"
}
# 409 Conflict nếu ID=1 đã tồn tại

Read

# Get by ID
GET /products/_doc/1

# Response
{
  "_index": "products",
  "_id": "1",
  "_version": 1,
  "found": true,
  "_source": {
    "name": "iPhone 15 Pro",
    "price": 999.99
  }
}

# Multi-get
GET /products/_mget
{
  "ids": ["1", "2", "3"]
}

# Chỉ lấy fields cần thiết
GET /products/_doc/1?_source_includes=name,price
GET /products/_doc/1?_source=false  # Không lấy _source

Update

# Update một phần (partial update) - chỉ thay đổi fields chỉ định
POST /products/_update/1
{
  "doc": {
    "price": 899.99,
    "in_stock": true
  }
}

# Update với script
POST /products/_update/1
{
  "script": {
    "source": "ctx._source.price *= params.discount",
    "params": {
      "discount": 0.9
    }
  }
}

# Upsert - update nếu tồn tại, insert nếu không
POST /products/_update/99
{
  "doc": { "name": "New Product", "price": 199.99 },
  "doc_as_upsert": true
}

# Update by query
POST /products/_update_by_query
{
  "query": {
    "term": { "category": "smartphones" }
  },
  "script": {
    "source": "ctx._source.discount_eligible = true"
  }
}

Delete

# Delete by ID
DELETE /products/_doc/1

# Delete by query
POST /products/_delete_by_query
{
  "query": {
    "range": {
      "price": { "lt": 10 }   # Xóa products giá dưới $10
    }
  }
}

Bulk API

Thực hiện nhiều operations trong một request - hiệu suất cao hơn nhiều.

POST /_bulk
{ "index": { "_index": "products", "_id": "1" } }
{ "name": "iPhone 15 Pro", "price": 999.99, "category": "smartphones" }
{ "index": { "_index": "products", "_id": "2" } }
{ "name": "Galaxy S24", "price": 899.99, "category": "smartphones" }
{ "create": { "_index": "products", "_id": "3" } }
{ "name": "Pixel 8", "price": 699.99, "category": "smartphones" }
{ "update": { "_index": "products", "_id": "1" } }
{ "doc": { "price": 949.99 } }
{ "delete": { "_index": "products", "_id": "99" } }

Bulk response:

{
  "took": 30,
  "errors": false,
  "items": [
    { "index": { "_id": "1", "result": "created", "status": 201 } },
    { "index": { "_id": "2", "result": "created", "status": 201 } },
    { "create": { "_id": "3", "result": "created", "status": 201 } },
    { "update": { "_id": "1", "result": "updated", "status": 200 } },
    { "delete": { "_id": "99", "result": "not_found", "status": 404 } }
  ]
}

Best Practices cho Bulk Indexing

# 1. Batch size: 5-15MB per bulk request
# 2. Tắt refresh trong quá trình bulk import lớn
PUT /products/_settings
{ "refresh_interval": "-1" }

# 3. Thực hiện bulk indexing
# ... nhiều bulk requests ...

# 4. Bật lại refresh
PUT /products/_settings
{ "refresh_interval": "1s" }

# 5. Force merge sau khi xong (tùy chọn)
POST /products/_forcemerge?max_num_segments=1

Pipeline Processing

Xử lý documents trước khi index với Ingest Pipelines.

# Tạo pipeline
PUT /_ingest/pipeline/product-pipeline
{
  "description": "Process products before indexing",
  "processors": [
    {
      "lowercase": {
        "field": "category"
      }
    },
    {
      "trim": {
        "field": "name"
      }
    },
    {
      "set": {
        "field": "indexed_at",
        "value": "{{{_ingest.timestamp}}}"
      }
    },
    {
      "convert": {
        "field": "price",
        "type": "float"
      }
    },
    {
      "remove": {
        "field": ["internal_id", "raw_data"],
        "ignore_missing": true
      }
    }
  ],
  "on_failure": [
    {
      "set": {
        "field": "error.message",
        "value": "{{ _ingest.on_failure_message }}"
      }
    }
  ]
}

# Sử dụng pipeline khi index
POST /products/_doc?pipeline=product-pipeline
{
  "name": "  iPhone 15 Pro  ",
  "price": "999.99",
  "category": "Smartphones"
}

# Đặt default pipeline cho index
PUT /products/_settings
{
  "default_pipeline": "product-pipeline"
}

Routing

# Mặc định: routing = document ID → hash → shard
# Custom routing: điều hướng related documents tới cùng shard

# Index với custom routing (ví dụ: theo user_id)
PUT /orders/_doc/order123?routing=user456
{
  "order_id": "order123",
  "user_id": "user456",
  "total": 150.00
}

# Search phải dùng cùng routing để chỉ query shard chứa data
GET /orders/_search?routing=user456
{
  "query": {
    "term": { "user_id": "user456" }
  }
}

# Lợi ích: Chỉ query 1 shard thay vì tất cả
# Chú ý: Nếu routing skewed → "hot shard" vấn đề

Basic Search với .NET

Cấu trúc Search Request

// Mọi search đều qua SearchAsync<T>
var response = await _es.SearchAsync<Product>(s => s
    .Index("products")
    .From(0)           // Offset (pagination)
    .Size(20)          // Số kết quả
    .Query(q => ...)   // Query DSL
    .Sort(so => ...)   // Sorting
    .Source(src => ...) // Chọn fields trả về
);

// Đọc kết quả
var products = response.Documents;          // IReadOnlyCollection<T>
var total    = response.Total;              // Tổng số documents match
var hits     = response.Hits;              // Kèm metadata (_score, _id, v.v.)
var maxScore = response.MaxScore;

// Tìm kiếm full-text trong một field
public async Task<List<Product>> SearchByNameAsync(string keyword)
{
    var response = await _es.SearchAsync<Product>(s => s
        .Query(q => q
            .Match(m => m
                .Field(f => f.Name)
                .Query(keyword)
                .Fuzziness(new Fuzziness(1)) // Cho phép 1 ký tự sai
                .Operator(Operator.And)      // Tất cả từ phải xuất hiện
            )
        )
        .Size(20)
    );

    return response.Documents.ToList();
}
// Equivalent JSON:
{
  "query": {
    "match": {
      "name": {
        "query": "iphone pro",
        "fuzziness": 1,
        "operator": "AND"
      }
    }
  }
}

Term Query - Exact Match

// Tìm chính xác - không phân tích (dành cho keyword fields)
public async Task<List<Product>> GetByCategoryAsync(string category)
{
    var response = await _es.SearchAsync<Product>(s => s
        .Query(q => q
            .Term(t => t
                .Field(f => f.Category)
                .Value(category)
            )
        )
    );

    return response.Documents.ToList();
}

// Terms - tìm trong danh sách giá trị
public async Task<List<Product>> GetByCategoriesAsync(IEnumerable<string> categories)
{
    var response = await _es.SearchAsync<Product>(s => s
        .Query(q => q
            .Terms(t => t
                .Field(f => f.Category)
                .Terms(new TermsQueryField(
                    categories.Select(c => FieldValue.String(c)).ToArray()
                ))
            )
        )
    );

    return response.Documents.ToList();
}

Range Query

public async Task<List<Product>> GetByPriceRangeAsync(
    decimal? minPrice,
    decimal? maxPrice)
{
    var response = await _es.SearchAsync<Product>(s => s
        .Query(q => q
            .Range(r => r
                .NumberRange(nr =>
                {
                    nr.Field(f => f.Price);
                    if (minPrice.HasValue) nr.Gte((double)minPrice.Value);
                    if (maxPrice.HasValue) nr.Lte((double)maxPrice.Value);
                    return nr;
                })
            )
        )
        .Sort(so => so.Field(f => f.Price, new FieldSort { Order = SortOrder.Asc }))
    );

    return response.Documents.ToList();
}

// Date range
public async Task<List<Product>> GetRecentProductsAsync(DateTime since)
{
    var response = await _es.SearchAsync<Product>(s => s
        .Query(q => q
            .Range(r => r
                .DateRange(dr => dr
                    .Field(f => f.CreatedAt)
                    .Gte(since)
                )
            )
        )
    );

    return response.Documents.ToList();
}

Bool Query - Kết hợp nhiều điều kiện

Bool query là query phổ biến nhất, kết hợp các queries khác:

must:     Điều kiện bắt buộc - ảnh hưởng relevance score
filter:   Điều kiện bắt buộc - KHÔNG ảnh hưởng score (nhanh hơn, được cache)
should:   Điều kiện tùy chọn - tăng score nếu match
must_not: Phủ định - loại bỏ documents match
public async Task<SearchResult<Product>> SearchProductsAsync(ProductSearchParams p)
{
    var response = await _es.SearchAsync<Product>(s => s
        .Query(q => q
            .Bool(b =>
            {
                // must: bắt buộc, ảnh hưởng score
                if (!string.IsNullOrEmpty(p.Keyword))
                {
                    b.Must(m => m
                        .MultiMatch(mm => mm
                            .Fields(Fields.FromExpressions<Product>(
                                f => f.Name,
                                f => f.Description
                            ))
                            .Query(p.Keyword)
                            .Type(TextQueryType.BestFields)
                        )
                    );
                }

                // filter: bắt buộc, KHÔNG ảnh hưởng score (nhanh hơn)
                var filters = new List<Action<QueryDescriptor<Product>>>();

                if (!string.IsNullOrEmpty(p.Category))
                    filters.Add(f => f.Term(t => t.Field(x => x.Category).Value(p.Category)));

                if (p.MinPrice.HasValue || p.MaxPrice.HasValue)
                    filters.Add(f => f.Range(r => r.NumberRange(nr =>
                    {
                        nr.Field(x => x.Price);
                        if (p.MinPrice.HasValue) nr.Gte((double)p.MinPrice.Value);
                        if (p.MaxPrice.HasValue) nr.Lte((double)p.MaxPrice.Value);
                        return nr;
                    })));

                if (p.InStockOnly)
                    filters.Add(f => f.Term(t => t.Field(x => x.InStock).Value(true)));

                if (filters.Count > 0)
                    b.Filter(filters.ToArray());

                // should: tùy chọn, tăng score
                if (p.PreferredBrands?.Any() == true)
                    b.Should(sh => sh
                        .Terms(t => t
                            .Field(f => f.Brand)
                            .Terms(new TermsQueryField(
                                p.PreferredBrands.Select(FieldValue.String).ToArray()
                            ))
                            .Boost(1.5f)
                        )
                    );

                return b;
            })
        )
        .From((p.Page - 1) * p.PageSize)
        .Size(p.PageSize)
        .Sort(so =>
        {
            switch (p.SortBy)
            {
                case "price_asc":
                    so.Field(f => f.Price, new FieldSort { Order = SortOrder.Asc });
                    break;
                case "price_desc":
                    so.Field(f => f.Price, new FieldSort { Order = SortOrder.Desc });
                    break;
                default: // relevance
                    so.Score(new ScoreSort { Order = SortOrder.Desc });
                    break;
            }
            return so;
        })
    );

    return new SearchResult<Product>
    {
        Items = response.Documents.ToList(),
        Total = response.Total,
        Page = p.Page,
        PageSize = p.PageSize,
    };
}

public record ProductSearchParams
{
    public string? Keyword { get; init; }
    public string? Category { get; init; }
    public decimal? MinPrice { get; init; }
    public decimal? MaxPrice { get; init; }
    public bool InStockOnly { get; init; }
    public List<string>? PreferredBrands { get; init; }
    public string SortBy { get; init; } = "relevance";
    public int Page { get; init; } = 1;
    public int PageSize { get; init; } = 20;
}

Sorting

var response = await _es.SearchAsync<Product>(s => s
    .Sort(so => so
        // Sort theo score trước
        .Score(new ScoreSort { Order = SortOrder.Desc })
        // Rồi theo price
        .Field(f => f.Price, new FieldSort { Order = SortOrder.Asc })
        // Sort theo text field phải dùng .keyword
        .Field("name.keyword", new FieldSort { Order = SortOrder.Asc })
    )
);

Fields Selection (Source filtering)

// Chỉ lấy fields cần thiết - giảm network traffic
var response = await _es.SearchAsync<Product>(s => s
    .Source(src => src
        .Includes(i => i.Fields(
            f => f.Name,
            f => f.Price,
            f => f.Category
        ))
        .Excludes(e => e.Fields(f => f.Description)) // Loại field nặng
    )
);

// Hoặc tắt _source hoàn toàn khi chỉ cần IDs
var response2 = await _es.SearchAsync<Product>(s => s
    .Source(false)
    .Query(q => q.MatchAll())
);
var ids = response2.Hits.Select(h => h.Id).ToList();

Handling Kết quả

var response = await _es.SearchAsync<Product>(s => s
    .Query(q => q.Match(m => m.Field(f => f.Name).Query("iphone")))
);

// Đọc documents
var products = response.Documents.ToList();

// Kèm metadata (score, id, highlights)
foreach (var hit in response.Hits)
{
    var product = hit.Source!;
    var score   = hit.Score;
    var id      = hit.Id;

    // Highlight snippets (nếu có config Highlight)
    if (hit.Highlight?.TryGetValue("name", out var highlights) == true)
    {
        var snippet = highlights.First();
        Console.WriteLine($"Highlight: {snippet}");
    }
}

// Tổng số kết quả
Console.WriteLine($"Total: {response.Total}, Returned: {response.Documents.Count}");

Query DSL Nâng cao với .NET

Multi-Match Query

Tìm kiếm trong nhiều fields cùng lúc.

public async Task<List<Product>> MultiFieldSearchAsync(string keyword)
{
    var response = await _es.SearchAsync<Product>(s => s
        .Query(q => q
            .MultiMatch(mm => mm
                .Query(keyword)
                .Fields(new[]
                {
                    "name^3",          // Boost name x3
                    "description^1",
                    "category^2",
                    "tags^1.5"
                })
                .Type(TextQueryType.BestFields) // Lấy field match tốt nhất
                // .Type(TextQueryType.MostFields)  // Cộng score tất cả fields
                // .Type(TextQueryType.CrossFields)  // Tìm terms trải qua nhiều fields
                .MinimumShouldMatch("75%") // Ít nhất 75% terms phải match
            )
        )
    );

    return response.Documents.ToList();
}

Fuzzy Query - Tìm kiếm mờ

Tìm kiếm chịu được lỗi chính tả.

public async Task<List<Product>> FuzzySearchAsync(string keyword)
{
    var response = await _es.SearchAsync<Product>(s => s
        .Query(q => q
            .Fuzzy(f => f
                .Field(field => field.Name)
                .Value(keyword)
                .Fuzziness(new Fuzziness("AUTO")) // AUTO: 0 cho 1-2 chars, 1 cho 3-5, 2 cho 5+
                .MaxExpansions(50)                 // Giới hạn số biến thể
                .PrefixLength(2)                   // Prefix không được fuzzy
            )
        )
    );

    return response.Documents.ToList();
}

// Trong Match query cũng có fuzziness
var response = await _es.SearchAsync<Product>(s => s
    .Query(q => q
        .Match(m => m
            .Field(f => f.Name)
            .Query("iphoone") // Gõ sai
            .Fuzziness(new Fuzziness("AUTO"))
        )
    )
);

Wildcard & Prefix

// Wildcard: * = nhiều ký tự, ? = một ký tự (cẩn thận hiệu suất)
var wildcardResponse = await _es.SearchAsync<Product>(s => s
    .Query(q => q
        .Wildcard(w => w
            .Field(f => f.Name)
            .Value("iphone*") // Bắt đầu bằng "iphone"
        )
    )
);

// Prefix: tìm documents bắt đầu bằng prefix (nhanh hơn wildcard)
var prefixResponse = await _es.SearchAsync<Product>(s => s
    .Query(q => q
        .Prefix(p => p
            .Field(f => f.Category)
            .Value("smart")  // "smartphones", "smartwatch", v.v.
        )
    )
);

Nested Query

Dùng khi field có type nested để giữ đúng quan hệ.

public class Product
{
    // ...
    public List<ProductReview> Reviews { get; set; } = [];
}

public class ProductReview
{
    public string UserId { get; set; } = string.Empty;
    public int Score { get; set; }
    public string Comment { get; set; } = string.Empty;
}

// Tìm products có review của Alice với score >= 4
public async Task<List<Product>> GetHighlyRatedByUserAsync(string userId, int minScore)
{
    var response = await _es.SearchAsync<Product>(s => s
        .Query(q => q
            .Nested(n => n
                .Path("reviews")                  // Đường dẫn đến nested field
                .Query(nq => nq
                    .Bool(b => b
                        .Must(
                            m => m.Term(t => t.Field("reviews.user_id").Value(userId)),
                            m => m.Range(r => r.NumberRange(nr => nr
                                .Field("reviews.score")
                                .Gte(minScore)
                            ))
                        )
                    )
                )
                .ScoreMode(ChildScoreMode.Max) // Lấy score cao nhất từ nested docs
            )
        )
    );

    return response.Documents.ToList();
}

Function Score Query - Tùy chỉnh Score

// Tăng score cho products in-stock và giá thấp
public async Task<List<Product>> SearchWithBoostingAsync(string keyword)
{
    var response = await _es.SearchAsync<Product>(s => s
        .Query(q => q
            .FunctionScore(fs => fs
                .Query(fq => fq
                    .Match(m => m.Field(f => f.Name).Query(keyword))
                )
                .Functions(
                    // Boost x2 nếu in stock
                    new FunctionScoreContainer
                    {
                        Filter = new TermQuery("in_stock") { Value = true },
                        Weight = 2.0f
                    },
                    // Giảm score theo giá (giá thấp hơn = score cao hơn)
                    new FunctionScoreContainer
                    {
                        FieldValueFactor = new FieldValueFactorScoreFunction
                        {
                            Field = "stock_quantity",
                            Factor = 1.2f,
                            Modifier = FieldValueFactorModifier.Log1P,
                            Missing = 1
                        }
                    }
                )
                .ScoreMode(FunctionScoreMode.Multiply)
                .BoostMode(FunctionBoostMode.Sum)
            )
        )
    );

    return response.Documents.ToList();
}

Search Template

Templates tái sử dụng được, store trên server.

// Lưu template
await _es.PutScriptAsync("product-search-template", ps => ps
    .Script(sc => sc
        .Lang("mustache")
        .Source("""
        {
          "query": {
            "bool": {
              "must": [
                {{#query}}
                { "match": { "name": "{{query}}" } }
                {{/query}}
              ],
              "filter": [
                {{#category}}
                { "term": { "category": "{{category}}" } }
                {{/category}}
              ]
            }
          },
          "size": "{{size}}",
          "from": "{{from}}"
        }
        """)
    )
);

// Sử dụng template
var response = await _es.SearchTemplateAsync<Product>(s => s
    .Index("products")
    .Id("product-search-template")
    .Params(p => p
        .Add("query", "iphone")
        .Add("category", "smartphones")
        .Add("size", 20)
        .Add("from", 0)
    )
);

Highlight - Đánh dấu từ khóa

public async Task<List<SearchHitDto>> SearchWithHighlightAsync(string keyword)
{
    var response = await _es.SearchAsync<Product>(s => s
        .Query(q => q
            .Match(m => m.Field(f => f.Name).Query(keyword))
        )
        .Highlight(h => h
            .Fields(
                hf => hf.Field(f => f.Name)
                    .NumberOfFragments(0)              // 0 = trả về toàn bộ field
                    .PreTags("<mark>")
                    .PostTags("</mark>"),
                hf => hf.Field(f => f.Description)
                    .NumberOfFragments(3)              // Lấy 3 đoạn chứa keyword
                    .FragmentSize(150)
                    .PreTags("<em>")
                    .PostTags("</em>")
            )
        )
    );

    return response.Hits.Select(hit => new SearchHitDto
    {
        Product = hit.Source!,
        NameHighlight = hit.Highlight?.GetValueOrDefault("name")?.FirstOrDefault(),
        DescriptionSnippets = hit.Highlight?.GetValueOrDefault("description")?.ToList() ?? [],
        Score = hit.Score ?? 0,
    }).ToList();
}

public record SearchHitDto
{
    public Product Product { get; init; } = default!;
    public string? NameHighlight { get; init; }
    public List<string> DescriptionSnippets { get; init; } = [];
    public double Score { get; init; }
}

Scroll API - Xuất dữ liệu lớn

Dùng khi cần duyệt qua số lượng lớn documents (> 10,000).

// Không dùng Scroll cho user-facing search - chỉ dùng cho export/processing
public async IAsyncEnumerable<Product> ScrollAllAsync(
    string index,
    [EnumeratorCancellation] CancellationToken ct = default)
{
    var response = await _es.SearchAsync<Product>(s => s
        .Index(index)
        .Query(q => q.MatchAll())
        .Size(1000)
        .Scroll(new Duration("2m")) // Giữ scroll context 2 phút
    , ct);

    while (response.Documents.Any())
    {
        foreach (var doc in response.Documents)
            yield return doc;

        response = await _es.ScrollAsync<Product>(scroll => scroll
            .ScrollId(response.ScrollId!)
            .Scroll(new Duration("2m"))
        , ct);
    }

    // Xóa scroll context khi xong
    await _es.ClearScrollAsync(cs => cs.ScrollId(response.ScrollId!));
}

// Sử dụng
await foreach (var product in ScrollAllAsync("products"))
{
    await ProcessProductAsync(product);
}

Aggregations với .NET

Giới thiệu

Aggregations là framework phân tích dữ liệu của Elasticsearch, tương đương GROUP BY + aggregation functions trong SQL.

Loại Aggregations:
- Metric:   Tính toán trên tập documents (min, max, avg, sum, stats)
- Bucket:   Nhóm documents (terms, range, date_histogram)
- Pipeline: Tính toán trên kết quả aggregation khác

Metric Aggregations

public async Task<ProductStats> GetProductStatsAsync(string? category = null)
{
    var response = await _es.SearchAsync<Product>(s => s
        .Size(0) // Không cần documents, chỉ cần aggs
        .Query(q =>
        {
            if (category is not null)
                q.Term(t => t.Field(f => f.Category).Value(category));
            else
                q.MatchAll();
            return q;
        })
        .Aggregations(a => a
            .Min("min_price", m => m.Field(f => f.Price))
            .Max("max_price", m => m.Field(f => f.Price))
            .Avg("avg_price", av => av.Field(f => f.Price))
            .Sum("total_value", sum => sum
                // price * stock_quantity per product
                .Script(sc => sc
                    .Source("doc['price'].value * doc['stock_quantity'].value")
                )
            )
            .Stats("price_stats", st => st.Field(f => f.Price))
            .Cardinality("unique_brands", c => c.Field(f => f.Brand))
            .ValueCount("in_stock_count", vc => vc.Field(f => f.InStock))
        )
    );

    var aggs = response.Aggregations!;

    return new ProductStats
    {
        MinPrice     = aggs.GetMin("min_price")?.Value ?? 0,
        MaxPrice     = aggs.GetMax("max_price")?.Value ?? 0,
        AvgPrice     = aggs.GetAvg("avg_price")?.Value ?? 0,
        TotalValue   = aggs.GetSum("total_value")?.Value ?? 0,
        UniqueBrands = (int)(aggs.GetCardinality("unique_brands")?.Value ?? 0),
    };
}

public record ProductStats
{
    public double MinPrice { get; init; }
    public double MaxPrice { get; init; }
    public double AvgPrice { get; init; }
    public double TotalValue { get; init; }
    public int UniqueBrands { get; init; }
}

Terms Aggregation - Nhóm theo giá trị

Tương đương GROUP BY category ORDER BY COUNT(*) DESC.

public async Task<List<CategoryFacet>> GetCategoryFacetsAsync()
{
    var response = await _es.SearchAsync<Product>(s => s
        .Size(0)
        .Aggregations(a => a
            .Terms("by_category", t => t
                .Field(f => f.Category) // Phải là keyword field
                .Size(20)               // Lấy top 20 categories
                .Order(new[] { TermsAggregationOrder.CountDescending })
                .MinDocCount(1)         // Chỉ trả về buckets có ít nhất 1 doc
            )
        )
    );

    var buckets = response.Aggregations!
        .GetStringTerms("by_category")?
        .Buckets ?? [];

    return buckets.Select(b => new CategoryFacet
    {
        Category = b.Key.ToString()!,
        Count    = (int)b.DocCount,
    }).ToList();
}

public record CategoryFacet(string Category, int Count);

Range Aggregation - Nhóm theo khoảng giá

public async Task<List<PriceBucket>> GetPriceFacetsAsync()
{
    var response = await _es.SearchAsync<Product>(s => s
        .Size(0)
        .Aggregations(a => a
            .Range("price_ranges", r => r
                .Field(f => f.Price)
                .Ranges(
                    new AggregationRange { To = 100, Key = "Under $100" },
                    new AggregationRange { From = 100, To = 500, Key = "$100 - $500" },
                    new AggregationRange { From = 500, To = 1000, Key = "$500 - $1000" },
                    new AggregationRange { From = 1000, Key = "Over $1000" }
                )
            )
        )
    );

    var buckets = response.Aggregations!
        .GetRange("price_ranges")?
        .Buckets ?? [];

    return buckets.Select(b => new PriceBucket
    {
        Label = b.Key,
        Count = (int)b.DocCount,
    }).ToList();
}

Date Histogram - Nhóm theo thời gian

public async Task<List<MonthlySales>> GetMonthlySalesAsync(int year)
{
    var response = await _es.SearchAsync<Order>(s => s
        .Size(0)
        .Query(q => q
            .Range(r => r.DateRange(dr => dr
                .Field(f => f.OrderedAt)
                .Gte(new DateTime(year, 1, 1))
                .Lt(new DateTime(year + 1, 1, 1))
            ))
        )
        .Aggregations(a => a
            .DateHistogram("monthly", dh => dh
                .Field(f => f.OrderedAt)
                .CalendarInterval(CalendarInterval.Month)
                .Format("yyyy-MM")
                .MinDocCount(0)  // Hiển thị tháng không có data
                .ExtendedBounds(new ExtendedBounds<FieldDateMath>(
                    new DateTime(year, 1, 1),
                    new DateTime(year, 12, 31)
                ))
            )
        )
    );

    var buckets = response.Aggregations!
        .GetDateHistogram("monthly")?
        .Buckets ?? [];

    return buckets.Select(b => new MonthlySales
    {
        Month = b.KeyAsString ?? "",
        Count = (int)b.DocCount,
    }).ToList();
}

Nested Aggregations - Kết hợp nhiều tầng

public async Task<List<CategorySummary>> GetCategorySummaryAsync()
{
    var response = await _es.SearchAsync<Product>(s => s
        .Size(0)
        .Aggregations(a => a
            .Terms("by_category", t => t
                .Field(f => f.Category)
                .Size(10)
                // Sub-aggregation trong mỗi bucket
                .Aggregations(sa => sa
                    .Avg("avg_price", avg => avg.Field(f => f.Price))
                    .Max("max_price", max => max.Field(f => f.Price))
                    .Sum("total_stock", sum => sum.Field(f => f.StockQuantity))
                    .Terms("top_brands", tb => tb
                        .Field(f => f.Brand)
                        .Size(3) // Top 3 brands trong mỗi category
                    )
                )
            )
        )
    );

    var categoryBuckets = response.Aggregations!
        .GetStringTerms("by_category")?
        .Buckets ?? [];

    return categoryBuckets.Select(bucket =>
    {
        var subAggs = bucket.Aggregations;
        var brandBuckets = subAggs.GetStringTerms("top_brands")?.Buckets ?? [];

        return new CategorySummary
        {
            Category  = bucket.Key.ToString()!,
            Count     = (int)bucket.DocCount,
            AvgPrice  = subAggs.GetAvg("avg_price")?.Value ?? 0,
            MaxPrice  = subAggs.GetMax("max_price")?.Value ?? 0,
            TopBrands = brandBuckets
                .Select(b => b.Key.ToString()!)
                .ToList(),
        };
    }).ToList();
}

public record CategorySummary
{
    public string Category { get; init; } = string.Empty;
    public int Count { get; init; }
    public double AvgPrice { get; init; }
    public double MaxPrice { get; init; }
    public List<string> TopBrands { get; init; } = [];
}

Pattern phổ biến trong e-commerce: search + filter + facets.

public async Task<FacetedSearchResult> FacetedSearchAsync(FacetedSearchParams p)
{
    var response = await _es.SearchAsync<Product>(s => s
        // Query chính
        .Query(q => q.Bool(b =>
        {
            if (!string.IsNullOrEmpty(p.Keyword))
                b.Must(m => m.MultiMatch(mm => mm
                    .Fields(new[] { "name^3", "description" })
                    .Query(p.Keyword)
                ));

            var filters = new List<Action<QueryDescriptor<Product>>>();
            if (p.Categories?.Any() == true)
                filters.Add(f => f.Terms(t => t
                    .Field(fld => fld.Category)
                    .Terms(new TermsQueryField(p.Categories.Select(FieldValue.String).ToArray()))
                ));
            if (p.MaxPrice.HasValue)
                filters.Add(f => f.Range(r => r.NumberRange(nr => nr
                    .Field(fld => fld.Price).Lte((double)p.MaxPrice.Value)
                )));
            if (filters.Any()) b.Filter(filters.ToArray());

            return b;
        }))
        // Pagination
        .From((p.Page - 1) * p.PageSize)
        .Size(p.PageSize)
        // Aggregations cho facets
        .Aggregations(a => a
            .Terms("categories", t => t.Field(f => f.Category).Size(20))
            .Terms("brands", t => t.Field(f => f.Brand).Size(20))
            .Range("price_ranges", r => r
                .Field(f => f.Price)
                .Ranges(
                    new AggregationRange { To = 100 },
                    new AggregationRange { From = 100, To = 500 },
                    new AggregationRange { From = 500, To = 1000 },
                    new AggregationRange { From = 1000 }
                )
            )
            .Stats("price_stats", st => st.Field(f => f.Price))
        )
    );

    var aggs = response.Aggregations!;

    return new FacetedSearchResult
    {
        Products = response.Documents.ToList(),
        Total    = response.Total,
        Facets = new FacetResults
        {
            Categories  = aggs.GetStringTerms("categories")?
                .Buckets.Select(b => new FacetItem(b.Key.ToString()!, (int)b.DocCount))
                .ToList() ?? [],
            Brands      = aggs.GetStringTerms("brands")?
                .Buckets.Select(b => new FacetItem(b.Key.ToString()!, (int)b.DocCount))
                .ToList() ?? [],
            PriceRanges = aggs.GetRange("price_ranges")?
                .Buckets.Select(b => new FacetItem(b.Key, (int)b.DocCount))
                .ToList() ?? [],
            PriceStats = new PriceStatsResult
            {
                Min = aggs.GetStats("price_stats")?.Min ?? 0,
                Max = aggs.GetStats("price_stats")?.Max ?? 0,
                Avg = aggs.GetStats("price_stats")?.Avg ?? 0,
            },
        },
    };
}

Analyzers & Tokenizers với .NET

Analyzer là gì?

Analyzer quyết định cách Elasticsearch phân tích text khi index khi search.

"Apple iPhone 15 Pro Max"
         ↓  Analyzer
  Tokenizer (tách thành tokens)
         ↓
  Token Filters (lowercase, stop words, stemming)
         ↓
  Inverted Index: ["apple", "iphone", "15", "pro", "max"]
Anatomy of an Analyzer:
┌─────────────────────────────────────────────────────────┐
│                      Analyzer                           │
│  ┌──────────────┐  ┌────────────┐  ┌────────────────┐  │
│  │ Char Filters │→ │ Tokenizer  │→ │ Token Filters  │  │
│  │ (pre-process)│  │ (split)    │  │ (transform)    │  │
│  └──────────────┘  └────────────┘  └────────────────┘  │
└─────────────────────────────────────────────────────────┘

Built-in Analyzers

// Test analyzer trong .NET
public async Task TestAnalyzerAsync()
{
    // Test một built-in analyzer
    var response = await _es.Indices.AnalyzeAsync(a => a
        .Analyzer("english")
        .Text("The quick brown foxes are running")
    );

    // Tokens: ["quick", "brown", "fox", "run"] - stemming + stop words
    foreach (var token in response.Tokens ?? [])
        Console.WriteLine($"{token.Token} (pos: {token.Position})");
}
AnalyzerMô tảVí dụ output
standardMặc định - tách theo Unicode, lowercase“Hello World” → [“hello”, “world”]
simpleTách theo ký tự không phải chữ“IP 192.168.1.1” → [“ip”]
whitespaceTách theo khoảng trắng“foo bar” → [“foo”, “bar”]
englishStemming tiếng Anh + stop words“running foxes” → [“run”, “fox”]
keywordKhông tách, giữ nguyên“Hello World” → [“Hello World”]

Custom Analyzer trong .NET

public async Task CreateIndexWithCustomAnalyzerAsync()
{
    await _es.Indices.CreateAsync<Product>("products", c => c
        .Settings(s => s
            .Analysis(a => a
                // 1. Định nghĩa Char Filters
                .CharFilters(cf => cf
                    .Mapping("remove_special_chars", m => m
                        .Mappings(new[] { "& => and", "@ => at" })
                    )
                )
                // 2. Định nghĩa Tokenizers
                .Tokenizers(t => t
                    .EdgeNGram("edge_ngram_tokenizer", en => en
                        .MinGram(2)
                        .MaxGram(10)
                        .TokenChars(new[] { TokenChar.Letter, TokenChar.Digit })
                    )
                )
                // 3. Định nghĩa Token Filters
                .TokenFilters(tf => tf
                    .Synonym("my_synonyms", syn => syn
                        .Synonyms(new[]
                        {
                            "phone, smartphone, mobile",
                            "laptop, notebook, computer"
                        })
                    )
                    .Stop("my_stop", st => st
                        .StopWords(new[] { "a", "an", "the", "is", "are" })
                    )
                    .Stemmer("english_stemmer", st => st
                        .Language("english")
                    )
                )
                // 4. Tạo Custom Analyzers
                .Analyzers(an => an
                    // Analyzer cho search (full pipeline)
                    .Custom("product_search_analyzer", ca => ca
                        .CharFilter(new[] { "remove_special_chars" })
                        .Tokenizer("standard")
                        .Filter(new[] { "lowercase", "my_stop", "my_synonyms", "english_stemmer" })
                    )
                    // Analyzer cho indexing (edge ngram để autocomplete)
                    .Custom("product_index_analyzer", ca => ca
                        .Tokenizer("edge_ngram_tokenizer")
                        .Filter(new[] { "lowercase" })
                    )
                    // Analyzer đơn giản cho autocomplete
                    .Custom("autocomplete_analyzer", ca => ca
                        .Tokenizer("standard")
                        .Filter(new[] { "lowercase", "autocomplete_filter" })
                    )
                )
                .TokenFilters(tf => tf
                    .EdgeNGram("autocomplete_filter", en => en
                        .MinGram(1)
                        .MaxGram(20)
                    )
                )
            )
        )
        .Mappings(m => m
            .Properties(p => p
                .Text(t => t
                    .Name(n => n.Name)
                    .Analyzer("product_index_analyzer")   // Index time
                    .SearchAnalyzer("product_search_analyzer") // Search time (khác!)
                    .Fields(f => f
                        .Keyword(k => k.Name("keyword"))
                        .Text(txt => txt
                            .Name("autocomplete")
                            .Analyzer("autocomplete_analyzer")
                            .SearchAnalyzer("standard")
                        )
                    )
                )
            )
        )
    );
}

Autocomplete với .NET

Pattern 1: Edge N-Gram Tokenizer

// Khi user gõ "ipho" → tìm "iphone"
// Edge N-Gram index "iphone": "i", "ip", "iph", "ipho", "iphon", "iphone"

public async Task<List<string>> AutocompleteAsync(string prefix)
{
    var response = await _es.SearchAsync<Product>(s => s
        .Source(src => src.Includes(i => i.Fields(f => f.Name)))
        .Query(q => q
            .Match(m => m
                .Field("name.autocomplete") // Dùng field với autocomplete analyzer
                .Query(prefix)
            )
        )
        .Size(10)
    );

    return response.Hits
        .Select(h => h.Source?.Name ?? "")
        .Where(n => !string.IsNullOrEmpty(n))
        .Distinct()
        .ToList();
}

Pattern 2: Search-as-you-type Field

// search_as_you_type: field type đặc biệt cho autocomplete
.Mappings(m => m
    .Properties(p => p
        .SearchAsYouType(s => s
            .Name(n => n.Name)
            .MaxShingleSize(4)
        )
    )
)

// Query
var response = await _es.SearchAsync<Product>(s => s
    .Query(q => q
        .MultiMatch(mm => mm
            .Fields(new[]
            {
                "name",
                "name._2gram",
                "name._3gram",
                "name._index_prefix"
            })
            .Query(prefix)
            .Type(TextQueryType.BoolPrefix)
        )
    )
    .Size(10)
);

Test Analyzer từ .NET

public async Task<List<string>> GetTokensAsync(
    string text,
    string analyzer = "standard",
    string? indexName = null)
{
    var response = indexName is not null
        ? await _es.Indices.AnalyzeAsync(a => a
            .Index(indexName)
            .Analyzer(analyzer)
            .Text(text)
          )
        : await _es.Indices.AnalyzeAsync(a => a
            .Analyzer(analyzer)
            .Text(text)
          );

    return response.Tokens?
        .Select(t => t.Token)
        .ToList() ?? [];
}

// Sử dụng để debug
var tokens = await GetTokensAsync(
    "The iPhones are amazing smartphones!",
    "english"
);
// Output: ["iphon", "amaz", "smartphon"]  (stemmed + stop words removed)

Language Analyzers

// Cho nội dung tiếng Việt - dùng icu_analysis hoặc custom
// Plugin: elasticsearch-analysis-icu

await _es.Indices.CreateAsync("products-vi", c => c
    .Settings(s => s
        .Analysis(a => a
            .Analyzers(an => an
                .Custom("vi_analyzer", ca => ca
                    .Tokenizer("icu_tokenizer")    // Unicode-aware tokenizer
                    .Filter(new[] { "icu_normalizer", "lowercase" })
                )
            )
        )
    )
    .Mappings(m => m
        .Properties(p => p
            .Text(t => t.Name("name").Analyzer("vi_analyzer"))
        )
    )
);

Performance Tuning với .NET

Indexing Performance

Tối ưu Bulk Indexing

// Cách tính batch size tối ưu: 5-15 MB per request
// Với document ~1KB → batch 5000-10000 docs
// Với document ~10KB → batch 500-1000 docs

public class BulkIndexOptions
{
    public int BatchSize { get; init; } = 500;
    public int MaxDegreeOfParallelism { get; init; } = 2;
    public bool DisableRefreshDuringIndex { get; init; } = true;
}

public async Task BulkIndexOptimizedAsync<T>(
    IEnumerable<T> documents,
    string indexName,
    Func<T, string> getId,
    BulkIndexOptions? options = null) where T : class
{
    options ??= new BulkIndexOptions();

    if (options.DisableRefreshDuringIndex)
    {
        await _es.Indices.PutSettingsAsync(indexName, s =>
            s.RefreshInterval(new Duration("-1"))
        );
    }

    try
    {
        var batches = documents
            .Chunk(options.BatchSize)
            .ToList();

        // Xử lý song song với giới hạn concurrency
        await Parallel.ForEachAsync(
            batches,
            new ParallelOptions { MaxDegreeOfParallelism = options.MaxDegreeOfParallelism },
            async (batch, ct) =>
            {
                await _es.BulkAsync(b => b
                    .Index(indexName)
                    .IndexMany(batch, (d, doc) => d.Id(getId(doc)))
                , ct);
            }
        );
    }
    finally
    {
        if (options.DisableRefreshDuringIndex)
        {
            await _es.Indices.PutSettingsAsync(indexName, s =>
                s.RefreshInterval(new Duration("1s"))
            );
            await _es.Indices.RefreshAsync(indexName);
        }
    }
}

Search Performance

Dùng Filter thay vì Must khi không cần Score

// ❌ Chậm: Tính score cho tất cả documents
var slow = await _es.SearchAsync<Product>(s => s
    .Query(q => q
        .Bool(b => b
            .Must(
                m => m.Term(t => t.Field(f => f.Category).Value("smartphones")),
                m => m.Term(t => t.Field(f => f.InStock).Value(true))
            )
        )
    )
);

// ✅ Nhanh: Filter không tính score, được cache
var fast = await _es.SearchAsync<Product>(s => s
    .Query(q => q
        .Bool(b => b
            .Filter(
                f => f.Term(t => t.Field(field => field.Category).Value("smartphones")),
                f => f.Term(t => t.Field(field => field.InStock).Value(true))
            )
        )
    )
);

Source Filtering - Chỉ lấy fields cần thiết

// ❌ Lấy toàn bộ document (kể cả description dài)
var allFields = await _es.SearchAsync<Product>(s => s.MatchAll());

// ✅ Chỉ lấy fields cần hiển thị trong list
var listFields = await _es.SearchAsync<Product>(s => s
    .Source(src => src
        .Includes(i => i.Fields(
            f => f.Name,
            f => f.Price,
            f => f.Category,
            f => f.InStock
        ))
    )
    .Query(q => q.MatchAll())
);

Pagination - Search After thay vì Deep From/Size

// ❌ Deep pagination - rất chậm (from: 10000)
// ES phải fetch 10000 + size docs rồi discard

// ✅ Search After - consistent, nhanh hơn
public async Task<SearchAfterResult<Product>> SearchAfterAsync(
    string keyword,
    long[]? searchAfter = null,
    int pageSize = 20)
{
    var response = await _es.SearchAsync<Product>(s =>
    {
        s.Query(q => q
            .Bool(b => b.Must(m => m.Match(mm => mm.Field(f => f.Name).Query(keyword))))
        )
        .Size(pageSize)
        .Sort(so => so
            .Score(new ScoreSort { Order = SortOrder.Desc })
            .Field(f => f.Id, new FieldSort { Order = SortOrder.Asc }) // Tie-breaker
        );

        if (searchAfter is not null)
            s.SearchAfter(searchAfter.Select(v => (FieldValue)v).ToArray());

        return s;
    });

    var lastHit = response.Hits.LastOrDefault();
    var nextSearchAfter = lastHit?.Sort?.Select(v => (long)v).ToArray();

    return new SearchAfterResult<Product>
    {
        Items = response.Documents.ToList(),
        NextCursor = nextSearchAfter,
        HasMore = response.Documents.Count == pageSize,
    };
}

public record SearchAfterResult<T>
{
    public List<T> Items { get; init; } = [];
    public long[]? NextCursor { get; init; }
    public bool HasMore { get; init; }
}

Request Cache & Query Cache

// Bật request cache cho heavy aggregation queries
var response = await _es.SearchAsync<Product>(s => s
    .RequestCache(true) // Cache kết quả ở node level (size=0 queries)
    .Size(0)
    .Aggregations(a => a
        .Terms("categories", t => t.Field(f => f.Category))
    )
);

// Preference - cùng user luôn hit cùng shard replica (tận dụng cache)
var response2 = await _es.SearchAsync<Product>(s => s
    .Preference($"user-{userId}") // Consistent routing cho cùng user
    .Query(q => q.MatchAll())
);

Index Settings Tối ưu

// Tối ưu index cho search-heavy workload
await _es.Indices.CreateAsync<Product>("products", c => c
    .Settings(s => s
        .NumberOfShards(1)              // Bắt đầu với 1, scale sau
        .NumberOfReplicas(1)            // 1 replica cho HA
        .RefreshInterval(new Duration("5s")) // Tăng refresh interval nếu near-real-time không cần
        .Analysis(a => a               // ... analyzers
        )
    )
);

// Optimize cho sau khi full reindex (merge segments)
await _es.Indices.ForcemergeAsync("products", f => f
    .MaxNumSegments(1)   // Gộp tất cả thành 1 segment (read-only index)
);

Timeout & Circuit Breaker

// Timeout per request
var response = await _es.SearchAsync<Product>(s => s
    .Timeout("5s")          // Partial results sau 5s thay vì wait mãi
    .Query(q => q.MatchAll())
);

// Global timeout trong client settings
var settings = new ElasticsearchClientSettings(new Uri("https://localhost:9200"))
    .RequestTimeout(TimeSpan.FromSeconds(30))
    .DeadTimeout(TimeSpan.FromMinutes(1))   // Node considered dead sau bao lâu
    .MaxDeadTimeout(TimeSpan.FromMinutes(5));

Monitoring qua .NET

public class ElasticsearchHealthChecker
{
    private readonly ElasticsearchClient _es;

    public async Task<ElasticsearchHealthReport> CheckHealthAsync()
    {
        var health = await _es.Cluster.HealthAsync();
        var stats = await _es.Indices.StatsAsync("products");

        return new ElasticsearchHealthReport
        {
            ClusterStatus   = health.Status.ToString(),
            ActiveShards    = (int)health.ActiveShards,
            UnassignedShards = (int)health.UnassignedShards,
            DocumentCount   = stats.All.Total?.Docs?.Count ?? 0,
            StoreSizeBytes  = stats.All.Total?.Store?.SizeInBytes ?? 0,
        };
    }
}

// Đăng ký Health Check trong ASP.NET Core
builder.Services.AddHealthChecks()
    .Add(new HealthCheckRegistration(
        "elasticsearch",
        sp =>
        {
            var es = sp.GetRequiredService<ElasticsearchClient>();
            return new ElasticsearchHealthCheck(es);
        },
        HealthStatus.Degraded,
        new[] { "database", "search" }
    ));

public class ElasticsearchHealthCheck : IHealthCheck
{
    private readonly ElasticsearchClient _es;

    public ElasticsearchHealthCheck(ElasticsearchClient es) => _es = es;

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken ct = default)
    {
        var ping = await _es.PingAsync(ct: ct);

        if (!ping.IsSuccess())
            return HealthCheckResult.Unhealthy("Elasticsearch unreachable");

        var health = await _es.Cluster.HealthAsync(ct: ct);

        return health.Status switch
        {
            HealthStatus.Green  => HealthCheckResult.Healthy("Elasticsearch healthy"),
            HealthStatus.Yellow => HealthCheckResult.Degraded("Elasticsearch degraded (yellow)"),
            HealthStatus.Red    => HealthCheckResult.Unhealthy("Elasticsearch unhealthy (red)"),
            _                   => HealthCheckResult.Unhealthy("Unknown status"),
        };
    }
}

Checklist Performance

Indexing:
□ Bulk indexing thay vì single document
□ Tắt refresh_interval trong bulk import lớn
□ ForceSegmentMerge sau reindex tĩnh
□ ScaledFloat cho tiền tệ thay vì double
□ Đừng dùng dynamic mapping trong production

Search:
□ Dùng filter thay vì must khi không cần score
□ Chỉ lấy fields cần thiết (source filtering)
□ Search After thay vì deep pagination
□ request_cache=true cho aggregation queries
□ Đặt timeout hợp lý

Cluster:
□ 1 primary shard nếu index < 50GB
□ Ít nhất 1 replica cho production
□ Heap size: 50% RAM, max 31GB
□ Monitor shard count (<1000 per node)
□ Dùng ILM để quản lý time-series data

Cluster Management với .NET

Cluster Health

public class ClusterManagementService
{
    private readonly ElasticsearchClient _es;

    public ClusterManagementService(ElasticsearchClient es) => _es = es;

    // Kiểm tra health
    public async Task<ClusterHealthSummary> GetHealthAsync()
    {
        var health = await _es.Cluster.HealthAsync(h => h
            .WaitForStatus(WaitForStatus.Green) // Chờ đến khi green (optional)
            .Timeout(new Duration("10s"))
        );

        return new ClusterHealthSummary
        {
            Status           = health.Status.ToString(),
            NumberOfNodes    = (int)health.NumberOfNodes,
            ActiveShards     = (int)health.ActiveShards,
            UnassignedShards = (int)health.UnassignedShards,
            IsHealthy        = health.Status == HealthStatus.Green,
        };
    }

    // Xem stats của index
    public async Task PrintIndexStatsAsync(string indexName)
    {
        var response = await _es.Indices.StatsAsync(indexName);
        var stats = response.Indices[indexName].Total;

        Console.WriteLine($"Documents: {stats?.Docs?.Count:N0}");
        Console.WriteLine($"Store size: {stats?.Store?.SizeInBytes / 1024 / 1024:N0} MB");
        Console.WriteLine($"Search rate: {stats?.Search?.QueryTotal:N0} queries total");
    }
}

public record ClusterHealthSummary
{
    public string Status { get; init; } = string.Empty;
    public int NumberOfNodes { get; init; }
    public int ActiveShards { get; init; }
    public int UnassignedShards { get; init; }
    public bool IsHealthy { get; init; }
}

Index Lifecycle Management (ILM)

ILM tự động quản lý vòng đời của time-series indices (logs, events).

public async Task SetupIlmPolicyAsync()
{
    // 1. Tạo ILM Policy
    await _es.IndexLifecycleManagement.PutLifecycleAsync("logs-policy", p => p
        .Policy(pol => pol
            .Phases(ph => ph
                // Hot phase: index đang active, write nhiều
                .Hot(hot => hot
                    .MinAge(new Duration("0ms"))
                    .Actions(a => a
                        .Rollover(ro => ro
                            .MaxSize("50gb")       // Rollover khi > 50GB
                            .MaxAge(new Duration("7d")) // Hoặc sau 7 ngày
                            .MaxDocs(10_000_000)   // Hoặc 10M docs
                        )
                        .SetPriority(sp => sp.Priority(100))
                    )
                )
                // Warm phase: không write, search thỉnh thoảng
                .Warm(warm => warm
                    .MinAge(new Duration("7d"))
                    .Actions(a => a
                        .Shrink(sh => sh.NumberOfShards(1)) // Giảm shard count
                        .ForceMerge(fm => fm.MaxNumSegments(1))
                        .SetPriority(sp => sp.Priority(50))
                    )
                )
                // Cold phase: search hiếm, long-term storage
                .Cold(cold => cold
                    .MinAge(new Duration("30d"))
                    .Actions(a => a
                        .Searchable_snapshot(ss => ss
                            .SnapshotRepository("my-repo")
                        )
                        .SetPriority(sp => sp.Priority(0))
                    )
                )
                // Delete phase
                .Delete(del => del
                    .MinAge(new Duration("90d"))
                    .Actions(a => a.Delete())
                )
            )
        )
    );

    // 2. Tạo Index Template gắn với ILM
    await _es.Indices.PutIndexTemplateAsync("logs-template", t => t
        .IndexPatterns(new[] { "logs-*" })
        .Template(tmpl => tmpl
            .Settings(s => s
                .NumberOfShards(1)
                .NumberOfReplicas(1)
                .Add("index.lifecycle.name", "logs-policy")
                .Add("index.lifecycle.rollover_alias", "logs")
            )
            .Mappings(m => m
                .Properties(p => p
                    .Date(d => d.Name("@timestamp"))
                    .Keyword(k => k.Name("level"))
                    .Text(txt => txt.Name("message"))
                    .Keyword(k => k.Name("service"))
                )
            )
        )
    );

    // 3. Tạo index đầu tiên với alias
    var bootstrapIndex = $"logs-{DateTime.UtcNow:yyyy.MM.dd}-000001";
    await _es.Indices.CreateAsync(bootstrapIndex, c => c
        .Aliases(a => a
            .Add("logs", al => al.IsWriteIndex(true))
        )
    );
}

// Ghi log - luôn dùng alias, không dùng index name trực tiếp
public async Task WriteLogAsync(LogEntry entry)
{
    await _es.IndexAsync(entry, i => i.Index("logs")); // alias
}

Alias Management

// Alias = tên thay thế cho index (hoặc nhiều indices)
// Dùng cho: blue-green deployment, zero-downtime reindex

public async Task SwapAliasAsync(string alias, string oldIndex, string newIndex)
{
    await _es.Indices.UpdateAliasesAsync(a => a
        .Actions(
            new RemoveIndexAction { Index = oldIndex, Alias = alias },
            new AddAction { Index = newIndex,  Alias = alias }
        )
    );
}

// Đọc alias hiện tại
public async Task<string?> GetIndexForAliasAsync(string alias)
{
    var response = await _es.Indices.GetAliasAsync(alias);
    return response.Indices.Keys.FirstOrDefault()?.ToString();
}

// Filtered alias - alias với filter query
await _es.Indices.PutAliasAsync("products_in_stock", "products", a => a
    .Filter(f => f.Term(t => t.Field("in_stock").Value(true)))
    .IsWriteIndex(false)
);

// Query qua filtered alias tự động áp dụng filter
var response = await _es.SearchAsync<Product>(s => s
    .Index("products_in_stock") // Chỉ products có in_stock=true
    .Query(q => q.MatchAll())
);

Snapshot & Restore

// Backup và restore
public class SnapshotService
{
    private readonly ElasticsearchClient _es;
    private const string RepoName = "my-backups";

    // Đăng ký repository (thực hiện một lần)
    public async Task RegisterRepositoryAsync(string storagePath)
    {
        await _es.Snapshot.CreateRepositoryAsync(RepoName, r => r
            .Repository(sr => sr
                .Fs(fs => fs.Settings(set => set.Location(storagePath)))
            )
        );
    }

    // Tạo snapshot
    public async Task CreateSnapshotAsync(string snapshotName, string[]? indices = null)
    {
        await _es.Snapshot.CreateAsync(RepoName, snapshotName, s => s
            .Indices(indices?.Select(i => (IndexName)i).ToArray() ?? Array.Empty<IndexName>())
            .IncludeGlobalState(false)
            .WaitForCompletion(true)
        );
    }

    // Restore snapshot
    public async Task RestoreSnapshotAsync(string snapshotName, string[]? indices = null)
    {
        await _es.Snapshot.RestoreAsync(RepoName, snapshotName, r => r
            .Indices(indices?.Select(i => (IndexName)i).ToArray() ?? Array.Empty<IndexName>())
            .WaitForCompletion(true)
        );
    }

    // Liệt kê snapshots
    public async Task<List<string>> ListSnapshotsAsync()
    {
        var response = await _es.Snapshot.GetAsync(RepoName, "_all");
        return response.Snapshots?
            .Select(s => $"{s.Snapshot} - {s.State} - {s.StartTime:yyyy-MM-dd}")
            .ToList() ?? [];
    }
}

ASP.NET Core Integration Pattern (Full)

// ElasticsearchOptions.cs
public class ElasticsearchOptions
{
    public const string SectionName = "Elasticsearch";

    public string Uri { get; set; } = "http://localhost:9200";
    public string? Username { get; set; }
    public string? Password { get; set; }
    public string DefaultIndex { get; set; } = "default";
    public bool EnableDebugMode { get; set; }
    public int RequestTimeoutSeconds { get; set; } = 30;
}

// Extensions/ElasticsearchExtensions.cs
public static class ElasticsearchExtensions
{
    public static IServiceCollection AddElasticsearch(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        var options = configuration
            .GetSection(ElasticsearchOptions.SectionName)
            .Get<ElasticsearchOptions>()
            ?? throw new InvalidOperationException("Elasticsearch options not configured");

        services.Configure<ElasticsearchOptions>(
            configuration.GetSection(ElasticsearchOptions.SectionName)
        );

        services.AddSingleton<ElasticsearchClient>(_ =>
        {
            var settings = new ElasticsearchClientSettings(new Uri(options.Uri))
                .DefaultIndex(options.DefaultIndex)
                .RequestTimeout(TimeSpan.FromSeconds(options.RequestTimeoutSeconds))
                .DefaultMappingFor<Product>(m => m.IndexName("products"))
                .DefaultMappingFor<Order>(m => m.IndexName("orders"))
                .DefaultMappingFor<LogEntry>(m => m.IndexName("logs"));

            if (!string.IsNullOrEmpty(options.Username))
                settings = settings.Authentication(
                    new BasicAuthentication(options.Username, options.Password!)
                );

            if (options.EnableDebugMode)
                settings = settings
                    .EnableDebugMode()
                    .DisableDirectStreaming();

            return new ElasticsearchClient(settings);
        });

        // Repositories
        services.AddScoped<IProductSearchRepository, ProductSearchRepository>();
        services.AddScoped<IProductIndexRepository, ProductIndexRepository>();

        // Health check
        services.AddHealthChecks()
            .AddCheck<ElasticsearchHealthCheck>("elasticsearch",
                tags: new[] { "search", "ready" });

        return services;
    }
}

// Program.cs
builder.Services.AddElasticsearch(builder.Configuration);

Kinh nghiệm làm việc (Professional Experience)

Giới thiệu

Phần này tổng hợp chi tiết các dự án thực tế đã tham gia, với trọng tâm vào backend development. Mỗi dự án được phân tích sâu về:

  • Bối cảnh & Yêu cầu: Bài toán thực tế cần giải quyết.
  • Kiến trúc & Công nghệ: Stack công nghệ và lý do lựa chọn.
  • Giải pháp kỹ thuật: Chi tiết implementation, design patterns, optimization techniques.
  • Kết quả định lượng: Số liệu cụ thể về performance improvement.
  • Câu hỏi phỏng vấn: Q&A chi tiết cho từng dự án.

Tổng quan dự án

1. KF Project | Optimizely CMS (04/2025 – 03/2026)

Vai trò: Fullstack Developer (chủ yếu Backend)

Dự án: Tích hợp Google Maps vào CMS Optimizely cho phép content editors cấu hình location trực tiếp.

Thành tựu nổi bật:

  • Google Maps integration với dynamic frontend interactions
  • Backend API design cho location management
  • Tối ưu caching cho map tiles và location data

📖 Xem chi tiết →


2. SKCC Project | FPT Software (11/2024 – 04/2025)

Vai trò: Backend Developer

Dự án: Hệ thống giám sát thiết bị công nghiệp với real-time data từ ElasticSearch.

Thành tựu nổi bật:

  • WPF monitoring application với real-time visualization
  • Configurable webhook engine với rule-based triggers
  • REST APIs cho integration với external systems
  • Xử lý hàng ngàn sensor data với low latency

📖 Xem chi tiết →


3. PTG.PPPlus3 | FPT Software (03/2023 – 10/2024)

Vai trò: Backend Developer

Dự án: Pension Management System (PMS) - Hệ thống quản lý lương hưu với 200+ team members.

Thành tựu nổi bật:

  • Report generation: Giảm 50% processing time với background jobs và message queues
  • Calculation optimization: Giảm 90% thời gian tính toán với pre-calculation strategy
  • Event-driven architecture: Azure Service Bus cho background processing
  • Scalable data retrieval: Batch queries, intermediate storage, pre-generated JSON

📖 Xem chi tiết →


Kỹ năng & Công nghệ chính

Backend Technologies

  • .NET Ecosystem: .NET Core, .NET 5/6/7, ASP.NET Core, Entity Framework Core
  • Message Queues: Azure Service Bus, RabbitMQ, Kafka
  • Databases: SQL Server, ElasticSearch, Redis
  • Background Processing: Hangfire, Quartz.NET, Hosted Services
  • API Design: RESTful APIs, GraphQL, gRPC

Architecture & Patterns

  • Architectural Patterns: Microservices, Event-Driven, Clean Architecture
  • Design Patterns: Repository, Unit of Work, Strategy, Observer, Factory
  • Messaging Patterns: Pub/Sub, Request/Reply, Competing Consumers
  • CQRS & Event Sourcing: Separation of read/write models

Performance Optimization

  • Caching Strategies: Distributed caching, in-memory caching, CDN
  • Database Optimization: Indexing, query optimization, partitioning
  • Async Processing: Background jobs, queue-based load leveling
  • Bulk Operations: Batch processing, bulk insert/update

Chuẩn bị phỏng vấn

Câu hỏi theo dự án

Mỗi dự án có bộ câu hỏi riêng tập trung vào:

  • Technical depth: Chi tiết implementation
  • Problem-solving: Cách xử lý challenges
  • System design: Kiến trúc và trade-offs
  • Impact: Kết quả định lượng

Câu hỏi tổng hợp

  • System design: Design a system tương tự dự án đã làm
  • Behavioral: Teamwork, conflict resolution, leadership
  • Career growth: Bài học, định hướng phát triển

📖 Xem câu hỏi phỏng vấn →


Phương pháp trình bày kinh nghiệm

STAR Method

Situation → Task → Action → Result

Ví dụ:

  • S: “Hệ thống báo cáo mất 20 giây để generate”
  • T: “Cần giảm thời gian xuống dưới 3 giây”
  • A: “Implement background jobs với Azure Service Bus, pre-generate JSON reports”
  • R: “Giảm 50% processing time, user satisfaction tăng 40%”

Tips quan trọng

  1. Số liệu cụ thể: Luôn có metrics để chứng minh impact
  2. Tập trung vào backend: Deep dive vào technical decisions
  3. Thể hiện learning mindset: Bài học rút ra và cải tiến
  4. Link với job requirements: Kinh nghiệm phù hợp với vị trí ứng tuyển

Mục lục chi tiết

  1. KF Project - Optimizely

    • Google Maps Integration
    • Optimizely CMS Architecture
    • Backend API Design
    • Caching Strategies
  2. SKCC Project - FPT

    • Real-time Monitoring System
    • ElasticSearch Integration
    • Webhook Engine Design
    • WPF & MVVM Architecture
  3. PTG.PPPlus3 - Pension System

    • Background Job Architecture
    • Message Queue Implementation
    • Report Generation Optimization
    • Pre-calculation Strategy
  4. Interview Q&A

    • Technical Questions by Project
    • System Design Questions
    • Behavioral Questions
    • Salary Negotiation

← Quay lại | Xem dự án đầu tiên →

KF Project | Optimizely CMS Integration

Thời gian: 04/2025 – 03/2026
Vai trò: Fullstack Developer (Backend focus)
Công ty: [Thông tin công ty]


Tổng quan dự án

Bối cảnh

Khách hàng cần một hệ thống CMS cho phép content editors quản lý các địa điểm kinh doanh (stores, offices, warehouses) với khả năng hiển thị trên bản đồ tương tác. Yêu cầu tích hợp Google Maps trực tiếp vào CMS để editors có thể:

  • Chọn vị trí trên bản đồ khi nhập địa chỉ
  • Xem trước locations trên map trước khi publish
  • Quản lý multiple locations với clustering
  • Tích hợp vào các trang landing page của từng khu vực

Yêu cầu chức năng

Functional Requirements

  1. Location Management

    • CRUD operations cho locations trong CMS
    • Import/export bulk locations từ CSV/Excel
    • Validation địa chỉ và tọa độ
  2. Map Integration

    • Hiển thị Google Maps trong CMS admin
    • Interactive marker placement
    • Clustering cho nhiều locations gần nhau
    • Custom marker styling theo category
  3. Frontend Display

    • Store locator page cho end users
    • Search & filter locations (by radius, category)
    • Direction integration (Google Directions API)
    • Responsive design cho mobile
  4. Caching & Performance

    • Cache map tiles và location data
    • Lazy loading cho markers
    • CDN integration cho static assets

Non-Functional Requirements

  • Performance: Page load < 2s, map render < 500ms
  • Scalability: Support 10,000+ locations
  • Availability: 99.9% uptime
  • SEO: Server-side rendering cho store locator pages

Kiến trúc & Công nghệ

Technology Stack

┌─────────────────────────────────────────────────────────────┐
│                      Frontend (React)                        │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│  │ Google Maps │  │  React      │  │  CSS Modules /      │  │
│  │ JavaScript  │  │  Components │  │  Styled Components  │  │
│  │  API        │  │             │  │                     │  │
│  └─────────────┘  └─────────────┘  └─────────────────────┘  │
└─────────────────────────────────────────────────────────────┘
                            ↕ HTTP/HTTPS
┌─────────────────────────────────────────────────────────────┐
│                 Backend (.NET + Optimizely CMS)              │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│  │  Optimizely │  │  REST API   │  │  Location Service   │  │
│  │  CMS        │  │  Controllers│  │                     │  │
│  └─────────────┘  └─────────────┘  └─────────────────────┘  │
│                            ↕                                  │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│  │  SQL Server │  │  Redis      │  │  Azure Blob Storage │  │
│  │  (Content)  │  │  (Cache)    │  │  (Static assets)    │  │
│  └─────────────┘  └─────────────┘  └─────────────────────┘  │
└─────────────────────────────────────────────────────────────┘

Components chi tiết

1. Optimizely CMS Backend

// Location Block - Custom Content Type
[ContentType(GUID = "xxx", Name = "LocationBlock")]
public class LocationBlock : BlockData
{
    [Display(Name = "Location Name")]
    public virtual string LocationName { get; set; }
    
    [Display(Name = "Address")]
    public virtual string Address { get; set; }
    
    [Display(Name = "Latitude")]
    public virtual double Latitude { get; set; }
    
    [Display(Name = "Longitude")]
    public virtual double Longitude { get; set; }
    
    [Display(Name = "Category")]
    public virtual string Category { get; set; }
    
    [Display(Name = "Opening Hours")]
    public virtual string OpeningHours { get; set; }
}

// Location Service
public interface ILocationService
{
    Task<LocationDto> GetLocationAsync(int locationId);
    Task<IEnumerable<LocationDto>> SearchLocationsAsync(SearchCriteria criteria);
    Task<LocationDto> CreateLocationAsync(LocationCreateRequest request);
    Task UpdateLocationAsync(int locationId, LocationUpdateRequest request);
    Task DeleteLocationAsync(int locationId);
    Task<IEnumerable<LocationDto>> ImportLocationsAsync(Stream csvStream);
}

2. REST API Controllers

[ApiController]
[Route("api/[controller]")]
public class LocationsController : ControllerBase
{
    private readonly ILocationService _locationService;
    private readonly IContentRepository _contentRepository;
    
    public LocationsController(
        ILocationService locationService,
        IContentRepository contentRepository)
    {
        _locationService = locationService;
        _contentRepository = contentRepository;
    }
    
    [HttpGet("{id}")]
    [ProducesResponseType(typeof(LocationDto), 200)]
    [ProducesResponseType(404)]
    public async Task<ActionResult<LocationDto>> GetLocation(int id)
    {
        var location = await _locationService.GetLocationAsync(id);
        if (location == null) return NotFound();
        return Ok(location);
    }
    
    [HttpGet("search")]
    [ProducesResponseType(typeof(IEnumerable<LocationDto>), 200)]
    public async Task<ActionResult<IEnumerable<LocationDto>>> SearchLocations(
        [FromQuery] SearchCriteria criteria)
    {
        var locations = await _locationService.SearchLocationsAsync(criteria);
        return Ok(locations);
    }
    
    [HttpPost]
    [ProducesResponseType(typeof(LocationDto), 201)]
    [ProducesResponseType(400)]
    public async Task<ActionResult<LocationDto>> CreateLocation(
        [FromBody] LocationCreateRequest request)
    {
        var location = await _locationService.CreateLocationAsync(request);
        return CreatedAtAction(nameof(GetLocation), new { id = location.Id }, location);
    }
}

3. Google Maps Integration

// React Component - Location Map
import { GoogleMap, useJsApiLoader, Marker, InfoWindow } from '@react-google-maps/api';

const LocationMap = ({ locations, onLocationSelect }) => {
  const { isLoaded } = useJsApiLoader({
    googleMapsApiKey: process.env.REACT_APP_GOOGLE_MAPS_API_KEY
  });
  
  const [selectedLocation, setSelectedLocation] = useState(null);
  const [mapCenter, setMapCenter] = useState({ lat: 21.0285, lng: 105.8542 });
  
  const onMapClick = useCallback((e) => {
    const newLocation = {
      lat: e.latLng.lat(),
      lng: e.latLng.lng()
    };
    onLocationSelect(newLocation);
  }, [onLocationSelect]);
  
  if (!isLoaded) return <div>Loading map...</div>;
  
  return (
    <GoogleMap
      center={mapCenter}
      zoom={12}
      onClick={onMapClick}
      options={{
        disableDefaultUI: false,
        clickableIcons: true,
        scrollwheel: true
      }}
    >
      {locations.map((location, index) => (
        <Marker
          key={location.id}
          position={{ lat: location.latitude, lng: location.longitude }}
          onClick={() => setSelectedLocation(location)}
          icon={{
            url: `/markers/${location.category}.png`,
            scaledSize: new google.maps.Size(30, 30)
          }}
        />
      ))}
      
      {selectedLocation && (
        <InfoWindow
          position={{ 
            lat: selectedLocation.latitude, 
            lng: selectedLocation.longitude 
          }}
          onCloseClick={() => setSelectedLocation(null)}
        >
          <div>
            <h3>{selectedLocation.name}</h3>
            <p>{selectedLocation.address}</p>
          </div>
        </InfoWindow>
      )}
    </GoogleMap>
  );
};

Giải pháp kỹ thuật chi tiết

1. Location Data Model

public class LocationDto
{
    public int Id { get; set; }
    public string LocationName { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string Country { get; set; }
    public string PostalCode { get; set; }
    
    // Geolocation
    public double Latitude { get; set; }
    public double Longitude { get; set; }
    public string GeoHash { get; set; } // For spatial queries
    
    // Metadata
    public string Category { get; set; } // Retail, Office, Warehouse
    public string Phone { get; set; }
    public string Email { get; set; }
    public string Website { get; set; }
    
    // Business Hours
    public Dictionary<DayOfWeek, BusinessHours> OpeningHours { get; set; }
    
    // Status
    public bool IsActive { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime UpdatedAt { get; set; }
}

public class BusinessHours
{
    public TimeSpan OpenTime { get; set; }
    public TimeSpan CloseTime { get; set; }
    public bool IsClosed { get; set; }
}

2. Geospatial Queries với SQL Server

public class LocationRepository : ILocationRepository
{
    private readonly ApplicationDbContext _context;
    
    public async Task<IEnumerable<LocationDto>> SearchByRadiusAsync(
        double latitude, 
        double longitude, 
        double radiusKm)
    {
        // Sử dụng SQL Server Spatial types
        var userLocation = new Point(longitude, latitude) { SRID = 4326 };
        
        var query = from loc in _context.Locations
                    let locationPoint = SqlFunctions.PointFromText(
                        $"POINT({loc.Longitude} {loc.Latitude})", 4326)
                    where locationPoint.STDistance(userLocation) <= radiusKm * 1000
                    orderby locationPoint.STDistance(userLocation)
                    select new LocationDto
                    {
                        Id = loc.Id,
                        LocationName = loc.LocationName,
                        Latitude = loc.Latitude,
                        Longitude = loc.Longitude,
                        Distance = locationPoint.STDistance(userLocation) / 1000 // km
                    };
        
        return await query.ToListAsync();
    }
    
    public async Task<IEnumerable<LocationDto>> SearchByBoundingBoxAsync(
        double minLat, double maxLat, 
        double minLng, double maxLng)
    {
        return await _context.Locations
            .Where(loc => 
                loc.Latitude >= minLat && loc.Latitude <= maxLat &&
                loc.Longitude >= minLng && loc.Longitude <= maxLng)
            .Select(loc => new LocationDto
            {
                Id = loc.Id,
                LocationName = loc.LocationName,
                Latitude = loc.Latitude,
                Longitude = loc.Longitude
            })
            .ToListAsync();
    }
}

3. Caching Strategy

public class CachedLocationService : ILocationService
{
    private readonly ILocationService _innerService;
    private readonly IDistributedCache _cache;
    private readonly ILogger<CachedLocationService> _logger;
    
    private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(30);
    private static readonly string CacheKeyPrefix = "location:";
    
    public CachedLocationService(
        ILocationService innerService,
        IDistributedCache cache,
        ILogger<CachedLocationService> logger)
    {
        _innerService = innerService;
        _cache = cache;
        _logger = logger;
    }
    
    public async Task<LocationDto> GetLocationAsync(int locationId)
    {
        var cacheKey = $"{CacheKeyPrefix}{locationId}";
        
        // Try get from cache
        var cachedLocation = await _cache.GetStringAsync(cacheKey);
        if (!string.IsNullOrEmpty(cachedLocation))
        {
            _logger.LogDebug("Cache hit for location {LocationId}", locationId);
            return JsonSerializer.Deserialize<LocationDto>(cachedLocation);
        }
        
        // Cache miss - get from database
        _logger.LogDebug("Cache miss for location {LocationId}", locationId);
        var location = await _innerService.GetLocationAsync(locationId);
        
        if (location != null)
        {
            // Store in cache
            var serialized = JsonSerializer.Serialize(location);
            await _cache.SetStringAsync(cacheKey, serialized, new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = CacheDuration,
                SlidingExpiration = TimeSpan.FromMinutes(10)
            });
        }
        
        return location;
    }
    
    public async Task<IEnumerable<LocationDto>> SearchLocationsAsync(SearchCriteria criteria)
    {
        // Cache key based on search criteria hash
        var criteriaHash = ComputeHash(criteria);
        var cacheKey = $"{CacheKeyPrefix}search:{criteriaHash}";
        
        var cached = await _cache.GetStringAsync(cacheKey);
        if (!string.IsNullOrEmpty(cached))
        {
            return JsonSerializer.Deserialize<IEnumerable<LocationDto>>(cached);
        }
        
        var results = await _innerService.SearchLocationsAsync(criteria);
        
        // Only cache if result count is reasonable
        if (results.Count() <= 100)
        {
            var serialized = JsonSerializer.Serialize(results);
            await _cache.SetStringAsync(cacheKey, serialized, new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15)
            });
        }
        
        return results;
    }
    
    private string ComputeHash(SearchCriteria criteria)
    {
        using var sha256 = SHA256.Create();
        var bytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(criteria));
        var hash = sha256.ComputeHash(bytes);
        return Convert.ToBase64String(hash);
    }
}

4. Bulk Import với Background Processing

public class LocationImportService : ILocationImportService
{
    private readonly ILocationService _locationService;
    private readonly IBackgroundJobClient _jobClient;
    private readonly IBlobStorageService _blobStorage;
    
    public async Task<ImportJobDto> StartImportAsync(Stream csvStream, string userId)
    {
        // Upload CSV to blob storage
        var blobName = $"imports/{Guid.NewGuid()}.csv";
        await _blobStorage.UploadAsync(blobName, csvStream);
        
        // Create import job
        var jobId = Guid.NewGuid().ToString();
        var job = new ImportJob
        {
            Id = jobId,
            BlobName = blobName,
            UserId = userId,
            Status = ImportJobStatus.Pending,
            CreatedAt = DateTime.UtcNow
        };
        
        // Queue background job
        await _jobClient.Enqueue<ILocationProcessor>(processor => 
            processor.ProcessImportAsync(jobId, blobName));
        
        return new ImportJobDto
        {
            JobId = jobId,
            Status = ImportJobStatus.Pending,
            EstimatedCompletionTime = DateTime.UtcNow.AddMinutes(5)
        };
    }
}

public interface ILocationProcessor
{
    Task ProcessImportAsync(string jobId, string blobName);
}

public class LocationProcessor : ILocationProcessor
{
    private readonly ILocationService _locationService;
    private readonly IBlobStorageService _blobStorage;
    private readonly IEmailService _emailService;
    
    public async Task ProcessImportAsync(string jobId, string blobName)
    {
        try
        {
            // Download CSV from blob
            var csvStream = await _blobStorage.DownloadAsync(blobName);
            
            // Parse CSV
            var locations = ParseCsv(csvStream);
            
            // Validate locations
            var validationResults = await ValidateLocationsAsync(locations);
            
            // Import valid locations
            var importedCount = 0;
            foreach (var location in locations.Where(l => l.IsValid))
            {
                await _locationService.CreateLocationAsync(location.ToRequest());
                importedCount++;
            }
            
            // Update job status
            await UpdateJobStatusAsync(jobId, ImportJobStatus.Completed, importedCount);
            
            // Send completion email
            await _emailService.SendImportCompletionEmailAsync(jobId, importedCount);
        }
        catch (Exception ex)
        {
            await UpdateJobStatusAsync(jobId, ImportJobStatus.Failed, 0, ex.Message);
            throw;
        }
    }
    
    private List<LocationImportDto> ParseCsv(Stream csvStream)
    {
        using var reader = new StreamReader(csvStream);
        using var csv = new CsvReader(reader, CultureInfo.InvariantCulture);
        return csv.GetRecords<LocationImportDto>().ToList();
    }
}

Thách thức & Giải pháp

Challenge 1: Đồng bộ dữ liệu giữa CMS và Map

Vấn đề: Khi nhiều editors cùng chỉnh sửa locations, dữ liệu có thể bị conflict.

Giải pháp:

public class OptimisticConcurrencyHandler
{
    public async Task<LocationDto> UpdateLocationAsync(
        int locationId, 
        LocationUpdateRequest request,
        string etag)
    {
        var location = await _context.Locations.FindAsync(locationId);
        if (location == null) return null;
        
        // Check ETag for concurrency
        var currentEtag = ComputeEtag(location);
        if (currentEtag != etag)
        {
            throw new ConcurrencyException(
                "Location was modified by another user. Please refresh and try again.");
        }
        
        // Apply updates
        location.Address = request.Address;
        location.Latitude = request.Latitude;
        location.Longitude = request.Longitude;
        location.UpdatedAt = DateTime.UtcNow;
        
        await _context.SaveChangesAsync();
        
        return MapToDto(location);
    }
    
    private string ComputeEtag(Location location)
    {
        var hash = SHA256.HashData(
            Encoding.UTF8.GetBytes($"{location.Id}:{location.UpdatedAt:O}"));
        return $"\"{Convert.ToBase64String(hash)}\"";
    }
}

Challenge 2: Performance với 10,000+ markers

Vấn đề: Render 10,000 markers trên map gây chậm trang.

Giải pháp:

  1. Server-side clustering: Group nearby locations trên server
  2. Viewport-based loading: Chỉ load markers trong viewport
  3. Lazy loading: Load thêm khi zoom in
// Viewport-based API
const loadMarkers = async (bounds, zoom) => {
  const response = await fetch(
    `/api/locations/search?minLat=${bounds.sw.lat}&maxLat=${bounds.ne.lat}` +
    `&minLng=${bounds.sw.lng}&maxLng=${bounds.ne.lng}&zoom=${zoom}`
  );
  return await response.json();
};

// Debounced map movement
const onMapMoveEnd = useCallback(debounce((map) => {
  const bounds = map.getBounds();
  const zoom = map.getZoom();
  loadMarkers(bounds, zoom).then(setMarkers);
}, 300), []);

Challenge 3: Geocoding API rate limits

Vấn đề: Google Geocoding API có giới hạn requests.

Giải pháp:

public class GeocodingService : IGeocodingService
{
    private readonly IGeocodingCache _cache;
    private readonly RateLimiter _rateLimiter;
    
    public async Task<GeocodeResult> GeocodeAsync(string address)
    {
        // Check cache first
        var cached = await _cache.GetGeocodeResultAsync(address);
        if (cached != null) return cached;
        
        // Rate limiting
        await _rateLimiter.WaitAsync();
        
        // Call Google Geocoding API
        var result = await _googleGeocodingClient.GeocodeAsync(address);
        
        // Cache result
        await _cache.SetGeocodeResultAsync(address, result, TimeSpan.FromDays(30));
        
        return result;
    }
}

public class RateLimiter
{
    private readonly SemaphoreSlim _semaphore = new(1, 1);
    private readonly TimeSpan _delay = TimeSpan.FromMilliseconds(200); // 5 req/s
    
    public async Task WaitAsync()
    {
        await _semaphore.WaitAsync();
        try
        {
            await Task.Delay(_delay);
        }
        finally
        {
            _semaphore.Release();
        }
    }
}

Kết quả & Impact

Metrics

MetricBeforeAfterImprovement
Page load time4.5s1.8s60% faster
Map render time2.1s0.4s81% faster
API response time (p95)850ms120ms86% faster
Cache hit rate-94%-
Import processing time15 min3 min80% faster

Business Impact

  • Content editors tiết kiệm 70% thời gian khi quản lý locations
  • User engagement tăng 35% với interactive store locator
  • Giảm 90% support tickets liên quan đến location data errors

Bài học kinh nghiệm

Technical Learnings

  1. Caching is critical: Luôn cache geospatial queries và map tiles
  2. Lazy loading matters: Không load tất cả markers cùng lúc
  3. Background jobs: Offload heavy processing khỏi request pipeline
  4. Rate limiting: Protect external API calls với rate limiters

Soft Skills

  1. Communication: Regular sync với stakeholders để understand requirements
  2. Documentation: Viết docs cho APIs và CMS integration
  3. Code review: Pair programming với junior developers

Câu hỏi phỏng vấn

Q1: Tại sao chọn Optimizely CMS thay vì custom solution?

A:

  • Time-to-market: Optimizely cung cấp sẵn content modeling, admin UI, workflow
  • Scalability: Built-in caching, CDN integration, multi-language support
  • Maintainability: Content editors quen với CMS UI, không cần training nhiều
  • Trade-off: Learning curve với proprietary API, nhưng worth it cho long-term

Q2: Bạn xử lý concurrency trong CMS như thế nào?

A:

  • Optimistic concurrency với ETags
  • Khi user save, check ETag match với current version
  • Nếu mismatch → show conflict dialog, let user decide (overwrite or refresh)
  • Version history: Optimizely tự động lưu versions, có thể rollback

Q3: Làm sao để test Google Maps integration?

A:

// Unit test với mock Google Maps API
public class GoogleMapsServiceTests
{
    [Fact]
    public async Task Geocode_ValidAddress_ReturnsCoordinates()
    {
        // Arrange
        var mockClient = new Mock<IGeocodingClient>();
        mockClient.Setup(c => c.GeocodeAsync("Hanoi, Vietnam"))
            .ReturnsAsync(new GeocodeResult { Latitude = 21.0285, Longitude = 105.8542 });
        
        var service = new GoogleMapsService(mockClient.Object);
        
        // Act
        var result = await service.GeocodeAsync("Hanoi, Vietnam");
        
        // Assert
        Assert.Equal(21.0285, result.Latitude, 4);
        Assert.Equal(105.8542, result.Longitude, 4);
    }
    
    // Integration test với Google Maps API sandbox
    [Fact]
    public async Task Geocode_IntegrationTest_RealApi()
    {
        var apiKey = Environment.GetEnvironmentVariable("GOOGLE_MAPS_TEST_API_KEY");
        var client = new GoogleGeocodingClient(apiKey);
        
        var result = await client.GeocodeAsync("Hanoi, Vietnam");
        
        Assert.NotNull(result);
        Assert.InRange(result.Latitude, 20.5, 21.5);
    }
}

Q4: Bạn optimize database queries cho geospatial data như thế nào?

A:

  1. Spatial indexes: Tạo index trên columns (Latitude, Longitude)
  2. Bounding box first: Filter nhanh với bounding box trước khi tính distance
  3. Covering indexes: Include các columns thường select
  4. Query optimization:
-- Tạo spatial index
CREATE INDEX IX_Locations_Location 
ON Locations (Latitude, Longitude) 
INCLUDE (LocationName, Category);

-- Query optimized
SELECT TOP 100 
    LocationName, Latitude, Longitude,
    Geography::Point(Latitude, Longitude, 4326).STDistance(
        Geography::Point(@userLat, @userLng, 4326)
    ) AS Distance
FROM Locations
WHERE Latitude BETWEEN @minLat AND @maxLat
  AND Longitude BETWEEN @minLng AND @maxLng
ORDER BY Distance;

Q5: Nếu phải redesign lại system, bạn sẽ thay đổi gì?

A:

  • Move to microservices: Tách Location Service ra khỏi CMS monolith
  • Event sourcing: Track location changes với event stream
  • GraphQL API: Cho frontend flexibility hơn REST
  • Real-time updates: SignalR cho live location updates
  • Better monitoring: Application Insights cho detailed telemetry

← Professional Experience | Xem dự án tiếp theo →

SKCC Project | Industrial Monitoring System

Thời gian: 11/2024 – 04/2025
Vai trò: Backend Developer
Công ty: FPT Software


Tổng quan dự án

Bối cảnh

Nhà máy sản xuất cần hệ thống giám sát thiết bị công nghiệp theo thời gian thực để:

  • Theo dõi trạng thái máy móc (nhiệt độ, áp suất, tốc độ, rung động)
  • Phát hiện bất thường và cảnh báo sớm
  • Tự động hóa quy trình thông báo khi có sự cố
  • Tích hợp với các hệ thống ERP, MES hiện có

Yêu cầu chức năng

Functional Requirements

  1. Real-time Monitoring

    • Hiển thị dữ liệu sensor từ hàng ngàn thiết bị
    • Update frequency: 1-5 giây
    • Historical data visualization (charts, trends)
  2. Alert & Notification

    • Configurable thresholds cho từng sensor type
    • Multi-channel notifications (Email, SMS, Webhook)
    • Alert escalation rules
  3. Webhook Engine

    • User-defined rules với custom conditions
    • Trigger external APIs khi có events
    • Retry mechanism cho failed webhooks
  4. Reporting & Analytics

    • Daily/Weekly/Monthly reports
    • OEE (Overall Equipment Effectiveness) calculations
    • Downtime analysis
  5. Integration APIs

    • REST APIs cho third-party systems
    • Data export (CSV, Excel, PDF)

Non-Functional Requirements

  • Latency: End-to-end < 2 seconds cho real-time data
  • Throughput: Xử lý 10,000+ events/giây
  • Availability: 99.9% uptime (critical for factory operations)
  • Scalability: Support thêm 50% devices mà không cần redesign
  • Data Retention: Lưu 2 years raw data, 5 years aggregated data

Kiến trúc & Công nghệ

Technology Stack

┌────────────────────────────────────────────────────────────────────┐
│                         Client Applications                         │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────────────┐  │
│  │  WPF Client  │  │  Web Dashboard│  │  Mobile App (Future)   │  │
│  │  (Operators) │  │  (Managers)   │  │                        │  │
│  └──────────────┘  └──────────────┘  └──────────────────────────┘  │
└────────────────────────────────────────────────────────────────────┘
         ↕ SignalR (Real-time)            ↕ REST API
┌────────────────────────────────────────────────────────────────────┐
│                      Backend Services (.NET Core)                   │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────────────┐  │
│  │  Data        │  │  Alert       │  │  Webhook                 │  │
│  │  Ingestion   │  │  Service     │  │  Engine                  │  │
│  │  Service     │  │              │  │                          │  │
│  └──────────────┘  └──────────────┘  └──────────────────────────┘  │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────────────┐  │
│  │  Reporting   │  │  Integration │  │  API Gateway             │  │
│  │  Service     │  │  Service     │  │  (Kong)                  │  │
│  └──────────────┘  └──────────────┘  └──────────────────────────┘  │
└────────────────────────────────────────────────────────────────────┘
         ↕                                       ↕
┌─────────────────────┐              ┌──────────────────────────────┐
│   Data Stores       │              │   Message Infrastructure     │
│  ┌──────────────┐   │              │  ┌────────────────────────┐  │
│  │  ElasticSearch│   │              │  │  Azure Service Bus     │  │
│  │  (Time-series)│   │              │  │  (Topics & Queues)     │  │
│  └──────────────┘   │              │  └────────────────────────┘  │
│  ┌──────────────┐   │              │                              │
│  │  SQL Server  │   │              │                              │
│  │  (Metadata)  │   │              │                              │
│  └──────────────┘   │              │                              │
│  ┌──────────────┐   │              │                              │
│  │  Redis       │   │              │                              │
│  │  (Cache)     │   │              │                              │
│  └──────────────┘   │              │                              │
└─────────────────────┘              └──────────────────────────────┘

Components chi tiết

1. Data Ingestion Pipeline

public interface ISensorDataService
{
    Task ProcessSensorDataAsync(SensorDataDto data);
    Task<IEnumerable<SensorDataDto>> GetHistoricalDataAsync(
        int deviceId, 
        DateTime from, 
        DateTime to);
    Task<SensorStatusDto> GetCurrentStatusAsync(int deviceId);
}

public class SensorDataIngestionService : ISensorDataService
{
    private readonly IElasticClient _elasticClient;
    private readonly ITopicClient _serviceBusTopic;
    private readonly ISensorCache _cache;
    
    public async Task ProcessSensorDataAsync(SensorDataDto data)
    {
        // 1. Validate data
        if (!IsValidSensorData(data))
        {
            throw new InvalidSensorDataException($"Invalid data from device {data.DeviceId}");
        }
        
        // 2. Store in ElasticSearch (time-series index)
        var indexName = $"sensor-data-{data.Timestamp:yyyy-MM}";
        await _elasticClient.IndexAsync(data, idx => idx.Index(indexName));
        
        // 3. Update current status cache
        await _cache.UpdateStatusAsync(data.DeviceId, new SensorStatus
        {
            LastValue = data.Value,
            LastUpdated = data.Timestamp,
            Status = DetermineStatus(data)
        });
        
        // 4. Check thresholds & publish events
        var alertEvent = await CheckThresholdsAsync(data);
        if (alertEvent != null)
        {
            await _serviceBusTopic.SendMessageAsync(new SensorAlertEvent
            {
                DeviceId = data.DeviceId,
                SensorType = data.SensorType,
                Value = data.Value,
                Threshold = alertEvent.Threshold,
                Severity = alertEvent.Severity,
                Timestamp = data.Timestamp
            });
        }
        
        // 5. Publish raw data event for other consumers
        await _serviceBusTopic.SendMessageAsync(new SensorDataEvent
        {
            DeviceId = data.DeviceId,
            Data = data
        });
    }
    
    public async Task<IEnumerable<SensorDataDto>> GetHistoricalDataAsync(
        int deviceId, 
        DateTime from, 
        DateTime to)
    {
        // Query ElasticSearch with date range
        var searchResponse = await _elasticClient.SearchAsync<SensorDataDto>(s => s
            .Index($"sensor-data-{from:yyyy-MM}")
            .Query(q => q
                .Bool(b => b
                    .Must(m => m.Term(t => t.DeviceId, deviceId))
                    .Filter(f => f
                        .DateRange(d => d
                            .Field(fld => fld.Timestamp)
                            .GreaterThanOrEquals(from)
                            .LessThanOrEquals(to)
                        )
                    )
                )
            )
            .Sort(sort => sort
                .Descending(d => d.Timestamp)
            )
            .Size(10000)
        );
        
        return searchResponse.Documents;
    }
    
    private SensorStatus DetermineStatus(SensorDataDto data)
    {
        // Business logic to determine if sensor is Normal, Warning, or Critical
        var thresholds = _thresholdCache.Get(data.DeviceId, data.SensorType);
        if (data.Value >= thresholds.Critical) return SensorStatus.Critical;
        if (data.Value >= thresholds.Warning) return SensorStatus.Warning;
        return SensorStatus.Normal;
    }
}

public class SensorDataDto
{
    public int DeviceId { get; set; }
    public string SensorType { get; set; } // Temperature, Pressure, Vibration
    public double Value { get; set; }
    public string Unit { get; set; } // Celsius, PSI, mm/s
    public DateTime Timestamp { get; set; }
    public string Quality { get; set; } // Good, Bad, Uncertain
}

2. Alert Service với Rule Engine

public interface IAlertService
{
    Task<AlertRuleDto> CreateRuleAsync(AlertRuleCreateRequest request);
    Task UpdateRuleAsync(int ruleId, AlertRuleUpdateRequest request);
    Task DeleteRuleAsync(int ruleId);
    Task<IEnumerable<AlertRuleDto>> GetRulesByDeviceAsync(int deviceId);
    Task ProcessAlertEventAsync(SensorAlertEvent alertEvent);
}

public class AlertRuleEngine : IAlertService
{
    private readonly RulesEngine _rulesEngine;
    private readonly IAlertRepository _alertRepository;
    private readonly INotificationService _notificationService;
    private readonly IWebhookDispatcher _webhookDispatcher;
    
    public async Task<AlertRuleDto> CreateRuleAsync(AlertRuleCreateRequest request)
    {
        // Validate rule expression
        var workflow = new Workflow
        {
            Name = $"Rule_{request.DeviceId}",
            Rules = new List<Rule>
            {
                new Rule
                {
                    Name = "ThresholdCheck",
                    Expression = request.RuleExpression, // e.g., "value > 100"
                    RuleExpressionType = RuleExpressionType.LambdaExpression
                }
            }
        };
        
        var validationResult = _rulesEngine.ValidateWorkflow(workflow);
        if (validationResult.Any(error => error.ErrorType == ErrorType.Compilation))
        {
            throw new InvalidRuleExpressionException(validationResult.First().Message);
        }
        
        // Save rule
        var rule = new AlertRule
        {
            DeviceId = request.DeviceId,
            SensorType = request.SensorType,
            RuleExpression = request.RuleExpression,
            Severity = request.Severity,
            NotificationChannels = request.NotificationChannels,
            WebhookUrl = request.WebhookUrl,
            IsActive = true,
            CreatedAt = DateTime.UtcNow
        };
        
        await _alertRepository.AddAsync(rule);
        return MapToDto(rule);
    }
    
    public async Task ProcessAlertEventAsync(SensorAlertEvent alertEvent)
    {
        // Get applicable rules
        var rules = await _alertRepository.GetActiveRulesAsync(
            alertEvent.DeviceId, 
            alertEvent.SensorType);
        
        foreach (var rule in rules)
        {
            // Evaluate rule
            var input = new { value = alertEvent.Value, threshold = alertEvent.Threshold };
            var result = await _rulesEngine.ExecuteActionAsync(rule.RuleExpression, input);
            
            if (result.IsSuccess)
            {
                // Create alert record
                var alert = new Alert
                {
                    RuleId = rule.Id,
                    DeviceId = alertEvent.DeviceId,
                    SensorType = alertEvent.SensorType,
                    Value = alertEvent.Value,
                    Severity = rule.Severity,
                    TriggeredAt = DateTime.UtcNow
                };
                
                await _alertRepository.CreateAlertAsync(alert);
                
                // Send notifications (parallel)
                var tasks = new List<Task>();
                
                if (rule.NotificationChannels.Contains("Email"))
                {
                    tasks.Add(_notificationService.SendEmailAlertAsync(alert));
                }
                
                if (rule.NotificationChannels.Contains("SMS"))
                {
                    tasks.Add(_notificationService.SendSmsAlertAsync(alert));
                }
                
                if (!string.IsNullOrEmpty(rule.WebhookUrl))
                {
                    tasks.Add(_webhookDispatcher.DispatchAsync(rule.WebhookUrl, alert));
                }
                
                await Task.WhenAll(tasks);
            }
        }
    }
}

public class AlertRuleCreateRequest
{
    public int DeviceId { get; set; }
    public string SensorType { get; set; }
    public string RuleExpression { get; set; } // e.g., "value > 100 && value < 200"
    public AlertSeverity Severity { get; set; }
    public List<string> NotificationChannels { get; set; }
    public string WebhookUrl { get; set; }
}

public enum AlertSeverity
{
    Low,
    Medium,
    High,
    Critical
}

3. Webhook Engine với Retry & Circuit Breaker

public interface IWebhookDispatcher
{
    Task DispatchAsync(string url, object payload);
    Task<WebhookLogDto> GetWebhookLogAsync(string logId);
}

public class ResilientWebhookDispatcher : IWebhookDispatcher
{
    private readonly HttpClient _httpClient;
    private readonly IWebhookLogRepository _logRepository;
    private readonly IBackgroundJobClient _jobClient;
    private readonly ILogger<ResilientWebhookDispatcher> _logger;
    
    // Circuit breaker policy
    private readonly AsyncCircuitBreakerPolicy _circuitBreaker;
    
    // Retry policy
    private readonly AsyncRetryPolicy _retryPolicy;
    
    public ResilientWebhookDispatcher(
        HttpClient httpClient,
        IWebhookLogRepository logRepository,
        IBackgroundJobClient jobClient,
        ILogger<ResilientWebhookDispatcher> logger)
    {
        _httpClient = httpClient;
        _logRepository = logRepository;
        _jobClient = jobClient;
        _logger = logger;
        
        // Configure retry policy
        _retryPolicy = Policy
            .Handle<HttpRequestException>()
            .OrResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
            .WaitAndRetryAsync(
                retryCount: 3,
                sleepDurationProvider: retry => TimeSpan.FromSeconds(Math.Pow(2, retry)),
                onRetry: (outcome, timespan, retryNumber, context) =>
                {
                    _logger.LogWarning(
                        "Webhook failed. Retry {RetryNumber} after {Timespan}. Status: {Status}",
                        retryNumber,
                        timespan,
                        outcome.Result?.StatusCode);
                }
            );
        
        // Configure circuit breaker
        _circuitBreaker = Policy
            .Handle<HttpRequestException>()
            .OrResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.ServiceUnavailable)
            .CircuitBreakerAsync(
                exceptionsAllowedBeforeBreaking: 5,
                durationOfBreak: TimeSpan.FromMinutes(5),
                onBreak: (outcome, duration) =>
                {
                    _logger.LogError("Circuit breaker opened for {Duration}. Reason: {Reason}",
                        duration, outcome.Exception?.Message);
                },
                onReset: () =>
                {
                    _logger.LogInformation("Circuit breaker reset to closed state.");
                }
            );
    }
    
    public async Task DispatchAsync(string url, object payload)
    {
        var logId = Guid.NewGuid().ToString();
        var log = new WebhookLog
        {
            Id = logId,
            Url = url,
            Payload = JsonSerializer.Serialize(payload),
            Status = WebhookStatus.Pending,
            CreatedAt = DateTime.UtcNow
        };
        
        await _logRepository.CreateAsync(log);
        
        try
        {
            // Execute with retry and circuit breaker
            var response = await _circuitBreaker.ExecuteAsync(
                async () => await _retryPolicy.ExecuteAsync(
                    async () =>
                    {
                        var content = new StringContent(
                            JsonSerializer.Serialize(payload),
                            Encoding.UTF8,
                            "application/json"
                        );
                        
                        return await _httpClient.PostAsync(url, content);
                    }
                )
            );
            
            // Update log
            log.Status = response.IsSuccessStatusCode 
                ? WebhookStatus.Success 
                : WebhookStatus.Failed;
            log.ResponseCode = (int?)response.StatusCode;
            log.ResponseBody = await response.Content.ReadAsStringAsync();
            log.CompletedAt = DateTime.UtcNow;
            
            await _logRepository.UpdateAsync(log);
            
            if (!response.IsSuccessStatusCode)
            {
                throw new HttpRequestException($"Webhook failed with status {response.StatusCode}");
            }
        }
        catch (BrokenCircuitException)
        {
            _logger.LogError("Circuit breaker open for webhook {Url}. Queuing for later.", url);
            
            // Queue for later retry when circuit closes
            await _jobClient.Schedule<IRetryWebhookJob>(
                job => job.RetryAsync(logId),
                TimeSpan.FromMinutes(10)
            );
            
            log.Status = WebhookStatus.CircuitOpen;
            await _logRepository.UpdateAsync(log);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Webhook dispatch failed for {Url}", url);
            
            log.Status = WebhookStatus.Failed;
            log.ErrorMessage = ex.Message;
            await _logRepository.UpdateAsync(log);
            
            // Schedule retry if not circuit breaker
            if (ex is not BrokenCircuitException)
            {
                await _jobClient.Schedule<IRetryWebhookJob>(
                    job => job.RetryAsync(logId),
                    TimeSpan.FromMinutes(5)
                );
            }
        }
    }
}

public interface IRetryWebhookJob
{
    Task RetryAsync(string logId);
}

public class RetryWebhookJob : IRetryWebhookJob
{
    private readonly IWebhookDispatcher _dispatcher;
    private readonly IWebhookLogRepository _logRepository;
    
    public async Task RetryAsync(string logId)
    {
        var log = await _logRepository.GetByIdAsync(logId);
        if (log == null) return;
        
        var payload = JsonSerializer.Deserialize<object>(log.Payload);
        await _dispatcher.DispatchAsync(log.Url, payload);
    }
}

4. WPF Client với Real-time Updates

// ViewModel với SignalR connection
public class MonitoringViewModel : INotifyPropertyChanged, IDisposable
{
    private readonly HubConnection _hubConnection;
    private readonly ISensorDataService _sensorService;
    private ObservableCollection<SensorViewModel> _sensors;
    
    public MonitoringViewModel(
        ISensorDataService sensorService,
        ILogger<MonitoringViewModel> logger)
    {
        _sensorService = sensorService;
        
        // Initialize SignalR connection
        _hubConnection = new HubConnectionBuilder()
            .WithUrl("https://api.skcc.com/sensorhub")
            .WithAutomaticReconnect(new RetryPolicy())
            .Build();
        
        // Register event handlers
        _hubConnection.On<SensorDataDto>("SensorDataReceived", data =>
        {
            UpdateSensorData(data);
        });
        
        _hubConnection.On<AlertDto>("AlertReceived", alert =>
        {
            ShowAlert(alert);
        });
        
        // Start connection
        _ = StartConnectionAsync();
    }
    
    private async Task StartConnectionAsync()
    {
        try
        {
            await _hubConnection.StartAsync();
            
            // Load initial data
            var sensors = await _sensorService.GetAllSensorsAsync();
            Sensors = new ObservableCollection<SensorViewModel>(
                sensors.Select(s => new SensorViewModel(s))
            );
        }
        catch (Exception ex)
        {
            // Handle connection error
            Console.WriteLine($"Connection failed: {ex.Message}");
        }
    }
    
    private void UpdateSensorData(SensorDataDto data)
    {
        // Update UI on dispatcher thread
        Application.Current.Dispatcher.Invoke(() =>
        {
            var sensor = Sensors.FirstOrDefault(s => s.DeviceId == data.DeviceId);
            if (sensor != null)
            {
                sensor.UpdateValue(data.Value, data.Timestamp);
            }
        });
    }
    
    private void ShowAlert(AlertDto alert)
    {
        Application.Current.Dispatcher.Invoke(() =>
        {
            // Show alert notification
            MessageBox.Show(
                $"Alert: {alert.SensorType} on Device {alert.DeviceId}\n" +
                $"Value: {alert.Value} {alert.Unit}\n" +
                $"Severity: {alert.Severity}",
                "Sensor Alert",
                MessageBoxButton.OK,
                alert.Severity == AlertSeverity.Critical 
                    ? MessageBoxImage.Error 
                    : MessageBoxImage.Warning
            );
        });
    }
    
    public void Dispose()
    {
        _hubConnection?.DisposeAsync().GetAwaiter().GetResult();
    }
    
    // INotifyPropertyChanged implementation
    public event PropertyChangedEventHandler PropertyChanged;
}

// Custom retry policy for SignalR
public class RetryPolicy : IRetryPolicy
{
    public TimeSpan? NextRetryDelay(DefaultRetryContext context)
    {
        // Exponential backoff: 0s, 2s, 10s, 30s, max 2 minutes
        if (context.PreviousRetryCount == 0) return TimeSpan.Zero;
        if (context.PreviousRetryCount == 1) return TimeSpan.FromSeconds(2);
        if (context.PreviousRetryCount == 2) return TimeSpan.FromSeconds(10);
        if (context.PreviousRetryCount == 3) return TimeSpan.FromSeconds(30);
        return TimeSpan.FromMinutes(2);
    }
}

ElasticSearch Optimization

Index Design

// Index template for time-series data
public class ElasticSearchIndexSetup
{
    public async Task SetupIndexTemplateAsync(IElasticClient client)
    {
        var templateName = "sensor-data-template";
        
        var templateResponse = await client.Indices.PutTemplateAsync(templateName, pt => pt
            .IndexPatterns("sensor-data-*")
            .Settings(s => s
                .NumberOfReplicas(1)
                .NumberOfShards(3)
                .RefreshInterval("5s") // Near real-time
            )
            .Map<m>(m
                .AutoMap<SensorDataDto>()
                .Properties(p => p
                    .Keyword(k => k
                        .Name(n => n.DeviceId)
                    )
                    .Keyword(k => k
                        .Name(n => n.SensorType)
                    )
                    .Date(d => d
                        .Name(n => n.Timestamp)
                    )
                    .Double(db => db
                        .Name(n => n.Value)
                    )
                )
            )
        );
        
        // Setup ILM (Index Lifecycle Management)
        var policyName = "sensor-data-policy";
        await client.XPack.IndexLifecycleManagement.PutLifecycleAsync(policyName, p => p
            .Policy(new LifecyclePolicy
            {
                Phases = new Phases
                {
                    Hot = new HotPhase
                    {
                        Actions = new Actions
                        {
                            RollOver = new RollOverAction
                            {
                                MaxAge = "30d",
                                MaxSize = "50gb"
                            }
                        }
                    },
                    Warm = new WarmPhase
                    {
                        MinAge = "30d",
                        Actions = new Actions
                        {
                            Shrink = new ShrinkAction
                            {
                                NumberOfShards = 1
                            },
                            SetPriority = new SetPriorityAction
                            {
                                Priority = 50
                            }
                        }
                    },
                    Cold = new ColdPhase
                    {
                        MinAge = "90d",
                        Actions = new Actions
                        {
                            SetPriority = new SetPriorityAction
                            {
                                Priority = 0
                            }
                        }
                    },
                    Delete = new DeletePhase
                    {
                        MinAge = "730d" // 2 years
                    }
                }
            })
        );
    }
}

Query Optimization

public class OptimizedSensorQueries
{
    private readonly IElasticClient _client;
    
    // Use search_after for deep pagination
    public async Task<IEnumerable<SensorDataDto>> GetHistoricalDataWithPaginationAsync(
        int deviceId,
        DateTime from,
        DateTime to,
        int pageSize = 1000,
        string? searchAfter = null)
    {
        ISearchResponse<SensorDataDto> searchResponse;
        
        if (searchAfter == null)
        {
            // First page
            searchResponse = await _client.SearchAsync<SensorDataDto>(s => s
                .Index($"sensor-data-{from:yyyy-MM}")
                .Query(q => q
                    .Bool(b => b
                        .Must(m => m.Term(t => t.DeviceId, deviceId))
                        .Filter(f => f
                            .DateRange(d => d
                                .Field(fld => fld.Timestamp)
                                .GreaterThanOrEquals(from)
                                .LessThanOrEquals(to)
                            )
                        )
                    )
                )
                .Sort(sort => sort
                    .Descending(d => d.Timestamp)
                )
                .Size(pageSize)
            );
        }
        else
        {
            // Subsequent pages with search_after
            var searchAfterValues = JsonSerializer.Deserialize<string[]>(searchAfter);
            searchResponse = await _client.SearchAsync<SensorDataDto>(s => s
                .Index($"sensor-data-{from:yyyy-MM}")
                .Query(q => q
                    .Bool(b => b
                        .Must(m => m.Term(t => t.DeviceId, deviceId))
                        .Filter(f => f
                            .DateRange(d => d
                                .Field(fld => fld.Timestamp)
                                .GreaterThanOrEquals(from)
                                .LessThanOrEquals(to)
                            )
                        )
                    )
                )
                .Sort(sort => sort
                    .Descending(d => d.Timestamp)
                )
                .SearchAfter(searchAfterValues)
                .Size(pageSize)
            );
        }
        
        return searchResponse.Documents;
    }
    
    // Aggregation for dashboard
    public async Task<SensorAggregationsDto> GetAggregationsAsync(
        int deviceId,
        DateTime from,
        DateTime to)
    {
        var response = await _client.SearchAsync<SensorDataDto>(s => s
            .Index($"sensor-data-{from:yyyy-MM}")
            .Query(q => q
                .Bool(b => b
                    .Must(m => m.Term(t => t.DeviceId, deviceId))
                    .Filter(f => f
                        .DateRange(d => d
                            .Field(fld => fld.Timestamp)
                            .GreaterThanOrEquals(from)
                            .LessThanOrEquals(to)
                        )
                    )
                )
            )
            .Size(0) // No hits needed
            .Aggregations(a => a
                .Average("avg_value", avg => avg.Field(f => f.Value))
                .Max("max_value", max => max.Field(f => f.Value))
                .Min("min_value", min => min.Field(f => f.Value))
                .StdDeviation("std_dev", std => std.Field(f => f.Value))
                .DateHistogram("timeline", dh => dh
                    .Field(f => f.Timestamp)
                    .CalendarInterval(CalendarInterval.Hour)
                    .Aggregations(aa => aa
                        .Average("hourly_avg", a => a.Field(f => f.Value))
                    )
                )
            )
        );
        
        return new SensorAggregationsDto
        {
            AverageValue = response.Aggregations.Average("avg_value")?.Value,
            MaxValue = response.Aggregations.Max("max_value")?.Value,
            MinValue = response.Aggregations.Min("min_value")?.Value,
            StdDeviation = response.Aggregations.StdDeviation("std_dev")?.StdDeviation,
            HourlyAverages = response.Aggregations
                .DateHistogram("timeline")
                .Buckets
                .Select(b => new HourlyAverage
                {
                    Timestamp = b.Key,
                    AverageValue = b.Average("hourly_avg")?.Value
                })
                .ToList()
        };
    }
}

Thách thức & Giải pháp

Challenge 1: Xử lý 10,000+ events/giây

Vấn đề: Khi nhà máy có hàng ngàn sensors gửi data mỗi giây, hệ thống bị quá tải.

Giải pháp:

// Batch processing với Channel
public class BatchedSensorDataProcessor
{
    private readonly Channel<SensorDataDto> _channel;
    private readonly List<Task> _workers;
    
    public BatchedSensorDataProcessor(int workerCount = 10, int batchSize = 100)
    {
        _channel = Channel.CreateBounded<SensorDataDto>(
            new BoundedChannelOptions(10000)
            {
                FullMode = BoundedChannelFullMode.Wait
            }
        );
        
        _workers = new List<Task>();
        for (int i = 0; i < workerCount; i++)
        {
            _workers.Add(ProcessBatchAsync(batchSize));
        }
    }
    
    public async Task QueueDataAsync(SensorDataDto data)
    {
        await _channel.Writer.WriteAsync(data);
    }
    
    private async Task ProcessBatchAsync(int batchSize)
    {
        var batch = new List<SensorDataDto>(batchSize);
        
        while (await _channel.Reader.WaitToReadAsync())
        {
            batch.Clear();
            
            // Collect batch
            while (batch.Count < batchSize && 
                   _channel.Reader.TryRead(out var data))
            {
                batch.Add(data);
            }
            
            if (batch.Count > 0)
            {
                // Bulk index to ElasticSearch
                await BulkIndexToElasticAsync(batch);
            }
        }
    }
    
    private async Task BulkIndexToElasticAsync(List<SensorDataDto> batch)
    {
        var bulkDescriptor = new BulkDescriptor();
        
        foreach (var data in batch)
        {
            var indexName = $"sensor-data-{data.Timestamp:yyyy-MM}";
            bulkDescriptor.Index<SensorDataDto>(idx => idx
                .Index(indexName)
                .Id($"{data.DeviceId}_{data.SensorType}_{data.Timestamp:O}")
                .Document(data)
            );
        }
        
        await _elasticClient.BulkAsync(bulkDescriptor);
    }
}

Challenge 2: Real-time visualization với hàng ngàn data points

Vấn đề: WPF client bị chậm khi render chart với quá nhiều points.

Giải pháp:

// Data downsampling cho visualization
public class TimeSeriesDownsampler
{
    // LTTB (Largest-Triangle-Three-Buckets) algorithm
    public List<SensorDataPoint> Downsample(
        List<SensorDataPoint> data, 
        int threshold)
    {
        if (data.Count <= threshold) return data;
        
        var downsampled = new List<SensorDataPoint>();
        int bucketSize = data.Count / threshold;
        
        int lastSelectedIndex = 0;
        downsampled.Add(data[0]); // Always include first point
        
        for (int i = 0; i < threshold - 2; i++)
        {
            int bucketStart = (i + 1) * bucketSize;
            int bucketEnd = (i + 2) * bucketSize;
            
            // Find point with largest triangle area
            var selectedPoint = FindLargestTrianglePoint(
                data, 
                data[lastSelectedIndex], 
                bucketStart, 
                bucketEnd
            );
            
            downsampled.Add(selectedPoint.point);
            lastSelectedIndex = selectedPoint.index;
        }
        
        downsampled.Add(data[^1]); // Always include last point
        return downsampled;
    }
    
    private (SensorDataPoint point, int index) FindLargestTrianglePoint(
        List<SensorDataPoint> data,
        SensorDataPoint lastPoint,
        int bucketStart,
        int bucketEnd)
    {
        double maxArea = -1;
        var selectedPoint = data[bucketStart];
        var selectedIndex = bucketStart;
        
        for (int i = bucketStart; i < bucketEnd && i < data.Count; i++)
        {
            // Calculate triangle area
            double area = Math.Abs(
                (lastPoint.Value * (data[i].Timestamp - data[bucketEnd].Timestamp) +
                 data[i].Value * (data[bucketEnd].Timestamp - lastPoint.Timestamp) +
                 data[bucketEnd].Value * (lastPoint.Timestamp - data[i].Timestamp)) / 2.0
            );
            
            if (area > maxArea)
            {
                maxArea = area;
                selectedPoint = data[i];
                selectedIndex = i;
            }
        }
        
        return (selectedPoint, selectedIndex);
    }
}

// Usage in WPF ViewModel
private void UpdateChartWithDownsampling(List<SensorDataPoint> rawData)
{
    var downsampler = new TimeSeriesDownsampler();
    var downsampled = downsampler.Downsample(rawData, threshold: 500);
    
    // Render only 500 points instead of 10,000
    ChartSeries.Points = new ObservableCollection<DataPoint>(
        downsampled.Select(p => new DataPoint(p.Timestamp, p.Value))
    );
}

Challenge 3: Webhook reliability với external systems

Vấn đề: External APIs có thể down, rate limit, hoặc trả về lỗi.

Giải pháp: (Xem Webhook Engine section ở trên với Circuit Breaker + Retry + Dead Letter Queue)


Kết quả & Impact

Metrics

MetricBeforeAfterImprovement
Event processing latency5.2s0.8s85% faster
Chart render time (10k points)3.5s0.3s91% faster
Webhook success rate78%99.5%27% improvement
Data retention30 days2 years24x longer
Alert delivery time15s2s87% faster

Business Impact

  • Giảm 60% downtime nhờ early warning system
  • Tiết kiệm 200 giờ/năm manual data collection
  • Tăng 40% OEE nhờ predictive maintenance

Bài học kinh nghiệm

Technical Learnings

  1. Time-series data cần special handling: Index rollover, downsampling, aggregation
  2. Circuit breaker là must-have: Khi tích hợp với external systems
  3. Batch processing > Real-time cho high volume: Trade-off giữa latency và throughput
  4. Monitoring & Alerting: Cần instrument mọi thứ từ day 1

Soft Skills

  1. Domain knowledge quan trọng: Hiểu factory operations để design đúng
  2. Stakeholder management: Operators vs Managers có different needs
  3. Documentation: Critical cho handover và maintenance

Câu hỏi phỏng vấn

Q1: Tại sao chọn ElasticSearch thay vì SQL Server cho sensor data?

A:

  • Write throughput: ElasticSearch handle tốt hơn cho write-heavy time-series
  • Aggregation performance: ES aggregations nhanh hơn SQL GROUP BY cho large datasets
  • Index lifecycle: ILM built-in, dễ dàng rollover và delete old data
  • Trade-off: ES không support transactions như SQL Server → dùng ES cho data, SQL cho metadata

Q2: Bạn xử lý data loss như thế nào khi system crash?

A:

// Write-ahead logging
public async Task ProcessSensorDataAsync(SensorDataDto data)
{
    // 1. Write to WAL first (atomic)
    await _walRepository.AppendAsync(new WalEntry
    {
        Data = data,
        Timestamp = DateTime.UtcNow
    });
    
    try
    {
        // 2. Process data
        await IndexToElasticAsync(data);
        
        // 3. Mark WAL entry as processed
        await _walRepository.MarkProcessedAsync(entry.Id);
    }
    catch (Exception ex)
    {
        // 4. On recovery, replay unprocessed WAL entries
        _logger.LogError(ex, "Processing failed, will replay from WAL");
        throw;
    }
}

// Recovery process
public async Task RecoverAsync()
{
    var unprocessedEntries = await _walRepository.GetUnprocessedAsync();
    
    foreach (var entry in unprocessedEntries)
    {
        await IndexToElasticAsync(entry.Data);
        await _walRepository.MarkProcessedAsync(entry.Id);
    }
}

Q3: Làm sao để test real-time system với hàng ngàn concurrent connections?

A:

// Load test với WebApplicationFactory
[Fact]
public async Task LoadTest_SensorDataIngestion_10kEventsPerSecond()
{
    using var factory = new CustomWebApplicationFactory();
    using var client = factory.CreateClient();
    
    var events = GenerateSensorEvents(10000);
    var tasks = events.Select(e => 
        client.PostAsJsonAsync("/api/sensor-data", e)
    );
    
    var stopwatch = Stopwatch.StartNew();
    await Task.WhenAll(tasks);
    stopwatch.Stop();
    
    var throughput = 10000 / stopwatch.Elapsed.TotalSeconds;
    
    Assert.True(throughput >= 5000, $"Expected >= 5000 events/s, got {throughput}");
}

// Integration test với TestContainers
[Fact]
public async Task IntegrationTest_ElasticSearchConnection()
{
    await using var container = new ElasticSearchBuilder()
        .Build();
    
    await container.StartAsync();
    
    var client = container.CreateClient();
    
    // Test indexing
    var doc = new SensorDataDto { /* ... */ };
    var response = await client.IndexAsync(doc);
    
    Assert.True(response.IsValid);
}

Q4: Bạn monitor system performance như thế nào?

A:

// Application Insights custom metrics
public class SensorDataIngestionService
{
    private readonly TelemetryClient _telemetry;
    private readonly Histogram _processingTime;
    private readonly Counter _eventsProcessed;
    
    public async Task ProcessSensorDataAsync(SensorDataDto data)
    {
        using var activity = _telemetry.StartOperation("ProcessSensorData");
        
        var stopwatch = Stopwatch.StartNew();
        
        try
        {
            await IndexToElasticAsync(data);
            
            _eventsProcessed.Add(1);
            _processingTime.Record(stopwatch.ElapsedMilliseconds);
            
            activity.SetTag("success", true);
        }
        catch (Exception ex)
        {
            _telemetry.TrackException(ex);
            activity.SetTag("success", false);
            throw;
        }
    }
}

// Dashboard với Grafana
// Query Prometheus:
// - Rate: rate(events_processed_total[1m])
// - Latency: histogram_quantile(0.95, processing_time_seconds_bucket)
// - Errors: rate(process_sensor_data_errors_total[1m])

Q5: Nếu phải scale lên 100,000 devices, bạn sẽ thay đổi gì?

A:

  1. Sharding strategy: Partition theo device_id range hoặc geo-region
  2. Edge computing: Pre-process data tại edge trước khi gửi về central
  3. Kafka thay vì Service Bus: Cho higher throughput
  4. Data tiering: Hot data in memory (Redis), warm in ES, cold in data lake
  5. Auto-scaling: Kubernetes HPA dựa trên queue depth

← KF Project - Optimizely | Xem dự án tiếp theo →

PTG.PPPlus3 | Pension Management System

Thời gian: 03/2023 – 10/2024
Vai trò: Backend Developer
Công ty: FPT Software
Dự án: Pension Management System (PMS) cho 200+ team members


Tổng quan dự án

Bối cảnh

Pension Management System (PMS) là hệ thống toàn diện quản lý lương hưu cho tổ chức lớn với:

  • Hàng trăm ngàn members
  • Phức tạp trong tính toán benefits
  • Nhiều regulatory requirements
  • Integration với nhiều external systems (tax, banking, HR)

Vấn đề chính

Hệ thống ban đầu gặp các vấn đề về performance:

  1. Report generation: Mất 15-20 giây để tạo báo cáo
  2. Calculation performance: Tính toán pension payout mất 5-10 giây cho mỗi member
  3. Database load: Heavy queries làm chậm hệ thống trong giờ cao điểm
  4. User experience: Users phải wait cho synchronous processing

Yêu cầu

Functional Requirements

  1. Member Management

    • CRUD operations cho members
    • Employment history tracking
    • Salary history và contribution tracking
  2. Benefit Calculation

    • Pension payout calculation (nhiều công thức phức tạp)
    • COLA (Cost of Living Adjustment)
    • Survivor benefits
    • Lump-sum payments
  3. Reporting

    • Monthly/Quarterly/Annual reports
    • Regulatory reports (government compliance)
    • Ad-hoc reports với custom filters
    • Export (PDF, Excel, CSV)
  4. Payment Processing

    • Monthly payment runs
    • Direct deposit integration
    • Payment adjustments
    • Arrears calculation
  5. Integration

    • HR systems (employee data)
    • Tax authorities (tax reporting)
    • Banks (payment processing)
    • Insurance companies

Non-Functional Requirements

  • Performance: Report generation < 3s, Calculation < 1s
  • Accuracy: 100% chính xác cho financial calculations
  • Auditability: Full audit trail cho mọi transaction
  • Scalability: Support 500,000+ members
  • Availability: 99.95% uptime (critical financial system)

Kiến trúc & Công nghệ

Technology Stack

┌─────────────────────────────────────────────────────────────────────┐
│                         Frontend (Angular)                           │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────────────┐   │
│  │  Member      │  │  Calculation │  │  Reporting               │   │
│  │  Management  │  │  Simulator   │  │  Dashboard               │   │
│  └──────────────┘  └──────────────┘  └──────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────┘
                            ↕ REST API / SignalR
┌─────────────────────────────────────────────────────────────────────┐
│                     Backend (.NET Core)                               │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────────────┐   │
│  │  Member      │  │  Calculation │  │  Report                  │   │
│  │  Service     │  │  Engine      │  │  Generation Service      │   │
│  └──────────────┘  └──────────────┘  └──────────────────────────┘   │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────────────┐   │
│  │  Payment     │  │  Integration │  │  Audit                   │   │
│  │  Service     │  │  Service     │  │  Service                 │   │
│  └──────────────┘  └──────────────┘  └──────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────┘
         ↕                                       ↕
┌─────────────────────┐              ┌──────────────────────────────┐
│   Data Stores       │              │   Background Processing      │
│  ┌──────────────┐   │              │  ┌────────────────────────┐  │
│  │  SQL Server  │   │              │  │  Azure Service Bus     │  │
│  │  (Primary DB)│   │              │  │  (Queues & Topics)     │  │
│  └──────────────┘   │              │  └────────────────────────┘  │
│  ┌──────────────┐   │              │         ↕                     │
│  │  Redis       │   │              │  ┌────────────────────────┐  │
│  │  (Cache)    │   │              │  │  Hangfire              │  │
│  └──────────────┘   │              │  │  (Job Scheduler)       │  │
│  ┌──────────────┐   │              │  └────────────────────────┘  │
│  │  Azure Blob  │   │              │                              │
│  │  (Reports)   │   │              │                              │
│  └──────────────┘   │              │                              │
└─────────────────────┘              └──────────────────────────────┘

Giải pháp kỹ thuật chi tiết

1. Background Job Architecture cho Report Generation

Vấn đề: Report generation synchronous làm blocking API responses.

Giải pháp: Event-driven background processing với progress tracking.

// Report Generation Request/Response
public class ReportGenerationRequest
{
    public string ReportType { get; set; } // MonthlyStatement, AnnualSummary, Regulatory
    public DateTime FromDate { get; set; }
    public DateTime ToDate { get; set; }
    public ReportFormat Format { get; set; } // PDF, Excel, CSV
    public ReportFilter Filters { get; set; }
    public string RequestedBy { get; set; }
}

public class ReportJobDto
{
    public string JobId { get; set; }
    public string ReportType { get; set; }
    public JobStatus Status { get; set; } // Pending, Processing, Completed, Failed
    public int ProgressPercentage { get; set; }
    public string BlobUrl { get; set; } // Download URL khi hoàn thành
    public DateTime CreatedAt { get; set; }
    public DateTime? CompletedAt { get; set; }
    public string ErrorMessage { get; set; }
}

// Report Service với Background Job
public interface IReportGenerationService
{
    Task<ReportJobDto> RequestReportAsync(ReportGenerationRequest request);
    Task<ReportJobDto> GetJobStatusAsync(string jobId);
    Task<byte[]> GetReportResultAsync(string jobId);
}

public class ReportGenerationService : IReportGenerationService
{
    private readonly IReportJobRepository _jobRepository;
    private readonly IBackgroundJobClient _jobClient;
    private readonly IBlobStorageService _blobStorage;
    private readonly ISignalRHubContext<ReportHub> _hubContext;
    
    public async Task<ReportJobDto> RequestReportAsync(ReportGenerationRequest request)
    {
        // Create job record
        var job = new ReportJob
        {
            Id = Guid.NewGuid().ToString(),
            ReportType = request.ReportType,
            Status = JobStatus.Pending,
            ProgressPercentage = 0,
            RequestData = JsonSerializer.Serialize(request),
            RequestedBy = request.RequestedBy,
            CreatedAt = DateTime.UtcNow
        };
        
        await _jobRepository.CreateAsync(job);
        
        // Queue background job với priority
        var queueName = GetQueueNameForReportType(request.ReportType);
        await _jobClient.Enqueue(
            queueName,
            () => ProcessReportJobAsync(job.Id, request)
        );
        
        // Return job info immediately (non-blocking)
        return MapToDto(job);
    }
    
    private async Task ProcessReportJobAsync(string jobId, ReportGenerationRequest request)
    {
        try
        {
            // Update status to Processing
            await UpdateJobProgressAsync(jobId, JobStatus.Processing, 0);
            
            // Step 1: Fetch data in batches
            await UpdateJobProgressAsync(jobId, JobStatus.Processing, 10, "Fetching data...");
            
            var batches = await GetDataBatchesAsync(request);
            var totalBatches = batches.Count;
            var processedBatches = 0;
            
            // Step 2: Process each batch
            var reportData = new List<ReportDataRow>();
            foreach (var batch in batches)
            {
                var batchData = await ProcessBatchAsync(batch, request);
                reportData.AddRange(batchData);
                
                processedBatches++;
                var progress = 10 + (processedBatches * 70 / totalBatches); // 10-80%
                await UpdateJobProgressAsync(jobId, JobStatus.Processing, progress);
                
                // Real-time progress update via SignalR
                await _hubContext.Clients.User(request.RequestedBy)
                    .SendAsync("ReportProgress", new
                    {
                        JobId = jobId,
                        Progress = progress,
                        Message = $"Processed {processedBatches}/{totalBatches} batches"
                    });
            }
            
            // Step 3: Generate report file
            await UpdateJobProgressAsync(jobId, JobStatus.Processing, 85, "Generating report...");
            
            byte[] reportFile;
            switch (request.Format)
            {
                case ReportFormat.PDF:
                    reportFile = await GeneratePdfReportAsync(reportData, request);
                    break;
                case ReportFormat.Excel:
                    reportFile = await GenerateExcelReportAsync(reportData, request);
                    break;
                case ReportFormat.CSV:
                    reportFile = await GenerateCsvReportAsync(reportData, request);
                    break;
                default:
                    throw new InvalidOperationException($"Unsupported format: {request.Format}");
            }
            
            // Step 4: Upload to Blob Storage
            await UpdateJobProgressAsync(jobId, JobStatus.Processing, 95, "Uploading report...");
            
            var blobName = $"reports/{jobId}/{GetFileName(request)}";
            var blobUrl = await _blobStorage.UploadAsync(blobName, reportFile, new Dictionary<string, string>
            {
                ["ContentType"] = GetContentType(request.Format),
                ["JobId"] = jobId,
                ["ReportType"] = request.ReportType
            });
            
            // Step 5: Mark job as completed
            await UpdateJobCompletedAsync(jobId, blobUrl);
            
            // Notify completion via SignalR
            await _hubContext.Clients.User(request.RequestedBy)
                .SendAsync("ReportCompleted", new { JobId = jobId, BlobUrl = blobUrl });
        }
        catch (Exception ex)
        {
            await UpdateJobFailedAsync(jobId, ex.Message);
            
            await _hubContext.Clients.User(request.RequestedBy)
                .SendAsync("ReportFailed", new { JobId = jobId, Error = ex.Message });
            
            throw;
        }
    }
    
    private async Task<List<DataBatch>> GetDataBatchesAsync(ReportGenerationRequest request)
    {
        // Split data into manageable batches (1000 records each)
        var totalCount = await GetTotalRecordCountAsync(request);
        var batchSize = 1000;
        var batches = new List<DataBatch>();
        
        for (int skip = 0; skip < totalCount; skip += batchSize)
        {
            batches.Add(new DataBatch
            {
                Skip = skip,
                Take = batchSize,
                Filters = request.Filters
            });
        }
        
        return batches;
    }
    
    private async Task<List<ReportDataRow>> ProcessBatchAsync(DataBatch batch, ReportGenerationRequest request)
    {
        // Use batching techniques để giảm database round trips
        var query = BuildReportQuery(request)
            .Skip(batch.Skip)
            .Take(batch.Take);
        
        // Use AsNoTracking() cho read-only queries
        // Use Bulk fetch thay vì N+1
        return await query
            .AsNoTracking()
            .ToListAsync();
    }
    
    private async Task UpdateJobProgressAsync(
        string jobId, 
        JobStatus status, 
        int progress, 
        string message = null)
    {
        await _jobRepository.UpdateAsync(jobId, new ReportJobUpdate
        {
            Status = status,
            ProgressPercentage = progress,
            StatusMessage = message
        });
    }
    
    private async Task UpdateJobCompletedAsync(string jobId, string blobUrl)
    {
        await _jobRepository.UpdateAsync(jobId, new ReportJobUpdate
        {
            Status = JobStatus.Completed,
            ProgressPercentage = 100,
            BlobUrl = blobUrl,
            CompletedAt = DateTime.UtcNow
        });
    }
    
    private async Task UpdateJobFailedAsync(string jobId, string errorMessage)
    {
        await _jobRepository.UpdateAsync(jobId, new ReportJobUpdate
        {
            Status = JobStatus.Failed,
            ErrorMessage = errorMessage
        });
    }
}

// Hangfire configuration cho priority queues
public static class HangfireConfig
{
    public static void Configure(IServiceCollection services)
    {
        services.AddHangfire(config => config
            .UseSqlServerStorage(connectionString)
            .UseFilter(new AutomaticRetryAttribute { Attempts = 3 })
            .UseFilter(new LogJobFilter()));
        
        services.AddHangfireServer(options =>
        {
            // Configure queues với priorities
            options.Queues = new[]
            {
                "critical",    // Priority 1: Payment processing
                "high",        // Priority 2: Regulatory reports
                "default",     // Priority 3: Regular reports
                "background"   // Priority 4: Cleanup jobs
            };
            options.WorkerCount = Environment.ProcessorCount * 2;
        });
    }
}

2. Pre-calculation Strategy cho Pension Calculations

Vấn đề: Tính toán pension payout phức tạp mất 5-10 giây cho mỗi member.

Giải pháp: Shift từ on-the-fly calculation sang pre-calculation với event-driven updates.

// Calculation Result Cache
public class PensionCalculationResult
{
    public int MemberId { get; set; }
    public decimal MonthlyBenefit { get; set; }
    public decimal LumpSumAmount { get; set; }
    public DateTime CalculationDate { get; set; }
    public string CalculationVersion { get; set; } // Version of calculation rules
    public Dictionary<string, object> InputData { get; set; } // Snapshot of inputs
    public Dictionary<string, decimal> Breakdown { get; set; } // Line items
    public bool IsValid { get; set; }
    public DateTime ValidUntil { get; set; }
}

// Pre-calculation Service
public interface IPensionPreCalculationService
{
    Task ScheduleCalculationAsync(int memberId, CalculationTrigger trigger);
    Task ScheduleBatchCalculationAsync(CalculationBatchRequest batch);
    Task<PensionCalculationResult> GetCachedResultAsync(int memberId);
    Task RecalculateAllAsync(DateTime effectiveDate);
}

public class PensionPreCalculationService : IPensionPreCalculationService
{
    private readonly ICalculationRepository _calcRepository;
    private readonly IMemberRepository _memberRepository;
    private readonly IBackgroundJobClient _jobClient;
    private readonly IEventBus _eventBus;
    private readonly IPensionCalculationEngine _calculationEngine;
    private readonly IDistributedCache _cache;
    
    // Schedule calculation khi có thay đổi
    public async Task ScheduleCalculationAsync(int memberId, CalculationTrigger trigger)
    {
        // Check if already scheduled
        var existing = await _calcRepository.GetPendingCalculationAsync(memberId);
        if (existing != null)
        {
            // Update existing with new trigger
            existing.Triggers.Add(trigger);
            await _calcRepository.UpdatePendingCalculationAsync(existing);
            return;
        }
        
        // Create new pending calculation
        var pendingCalc = new PendingCalculation
        {
            MemberId = memberId,
            Triggers = new List<CalculationTrigger> { trigger },
            ScheduledAt = DateTime.UtcNow,
            Status = CalculationStatus.Pending
        };
        
        await _calcRepository.CreatePendingCalculationAsync(pendingCalc);
        
        // Schedule job (run immediately or batch)
        if (trigger.IsUrgent)
        {
            // Urgent: Calculate within 5 minutes
            await _jobClient.Enqueue<ICalculationProcessor>(processor =>
                processor.CalculateMemberAsync(memberId)
            );
        }
        else
        {
            // Non-urgent: Batch with others, run at off-peak hours
            await _jobClient.Schedule<ICalculationProcessor>(processor =>
                processor.CalculateMemberAsync(memberId),
                TimeSpan.FromMinutes(30)
            );
        }
    }
    
    // Batch calculation cho scheduled jobs
    public async Task ScheduleBatchCalculationAsync(CalculationBatchRequest batch)
    {
        // Group by calculation type
        var groupedJobs = batch.MemberIds.GroupBy(m => m.CalculationType);
        
        foreach (var group in groupedJobs)
        {
            // Create batch job
            var batchJob = new CalculationBatchJob
            {
                Id = Guid.NewGuid().ToString(),
                CalculationType = group.Key,
                MemberIds = group.Select(m => m.MemberId).ToList(),
                EffectiveDate = batch.EffectiveDate,
                Status = BatchJobStatus.Scheduled,
                ScheduledRunTime = batch.RunAt
            };
            
            await _calcRepository.CreateBatchJobAsync(batchJob);
            
            // Schedule with Hangfire
            await _jobClient.Schedule<ICalculationProcessor>(processor =>
                processor.ProcessBatchAsync(batchJob.Id),
                batch.RunAt - DateTime.UtcNow
            );
        }
    }
    
    // Get cached result
    public async Task<PensionCalculationResult> GetCachedResultAsync(int memberId)
    {
        // Try cache first
        var cacheKey = $"pension:calc:{memberId}";
        var cached = await _cache.GetAsync<PensionCalculationResult>(cacheKey);
        if (cached != null && cached.IsValid && cached.ValidUntil > DateTime.UtcNow)
        {
            return cached;
        }
        
        // Fallback to database
        var result = await _calcRepository.GetLatestCalculationAsync(memberId);
        if (result != null)
        {
            // Cache for next time
            await _cache.SetAsync(cacheKey, result, TimeSpan.FromHours(1));
        }
        
        return result;
    }
    
    // Recalculate all members (for rule changes)
    public async Task RecalculateAllAsync(DateTime effectiveDate)
    {
        var memberIds = await _memberRepository.GetAllMemberIdsAsync();
        
        // Chunk into batches of 1000
        var batches = memberIds.Chunk(1000).Select((chunk, index) => new CalculationBatchRequest
        {
            MemberIds = chunk.Select(id => new MemberCalculationItem { MemberId = id }).ToList(),
            EffectiveDate = effectiveDate,
            RunAt = effectiveDate.AddDays(-1).AddHours(2) // Run at 2 AM before effective date
        }).ToList();
        
        foreach (var batch in batches)
        {
            await ScheduleBatchCalculationAsync(batch);
        }
    }
}

// Calculation Processor
public interface ICalculationProcessor
{
    Task CalculateMemberAsync(int memberId);
    Task ProcessBatchAsync(string batchJobId);
}

public class PensionCalculationProcessor : ICalculationProcessor
{
    private readonly IPensionCalculationEngine _engine;
    private readonly ICalculationRepository _repository;
    private readonly IDistributedCache _cache;
    private readonly ILogger<PensionCalculationProcessor> _logger;
    
    public async Task CalculateMemberAsync(int memberId)
    {
        try
        {
            // Fetch member data
            var member = await _repository.GetMemberDataAsync(memberId);
            if (member == null) return;
            
            // Get pending triggers
            var pendingCalc = await _repository.GetPendingCalculationAsync(memberId);
            if (pendingCalc == null) return;
            
            // Run calculation
            var result = await _engine.CalculateAsync(member, pendingCalc.Triggers);
            
            // Save result
            await _repository.SaveCalculationResultAsync(result);
            
            // Update cache
            var cacheKey = $"pension:calc:{memberId}";
            await _cache.SetAsync(cacheKey, result, TimeSpan.FromHours(24));
            
            // Mark pending as completed
            await _repository.MarkCalculationCompletedAsync(memberId);
            
            // Publish event for downstream systems
            await _eventBus.PublishAsync(new PensionCalculatedEvent
            {
                MemberId = memberId,
                NewBenefit = result.MonthlyBenefit,
                EffectiveDate = result.CalculationDate,
                PreviousBenefit = pendingCalc.PreviousBenefit
            });
            
            _logger.LogInformation("Calculated pension for member {MemberId}. New benefit: {Benefit}", 
                memberId, result.MonthlyBenefit);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to calculate pension for member {MemberId}", memberId);
            
            // Mark as failed for manual review
            await _repository.MarkCalculationFailedAsync(memberId, ex.Message);
            throw;
        }
    }
    
    public async Task ProcessBatchAsync(string batchJobId)
    {
        var batchJob = await _repository.GetBatchJobAsync(batchJobId);
        if (batchJob == null) return;
        
        _logger.LogInformation("Starting batch calculation job {JobId} with {Count} members", 
            batchJobId, batchJob.MemberIds.Count);
        
        var stopwatch = Stopwatch.StartNew();
        var successCount = 0;
        var failureCount = 0;
        
        foreach (var memberId in batchJob.MemberIds)
        {
            try
            {
                await CalculateMemberAsync(memberId);
                successCount++;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to calculate member {MemberId} in batch", memberId);
                failureCount++;
            }
            
            // Update batch progress
            var progress = (successCount + failureCount) * 100 / batchJob.MemberIds.Count;
            await _repository.UpdateBatchJobProgressAsync(batchJobId, progress);
        }
        
        stopwatch.Stop();
        
        await _repository.CompleteBatchJobAsync(batchJobId, successCount, failureCount, stopwatch.Elapsed);
        
        _logger.LogInformation(
            "Batch job {JobId} completed. Success: {Success}, Failed: {Failed}, Duration: {Duration}", 
            batchJobId, successCount, failureCount, stopwatch.Elapsed);
    }
}

// Calculation Engine với Strategy Pattern
public interface IPensionCalculationEngine
{
    Task<PensionCalculationResult> CalculateAsync(MemberData member, List<CalculationTrigger> triggers);
}

public class PensionCalculationEngine : IPensionCalculationEngine
{
    private readonly IEnumerable<ICalculationStrategy> _strategies;
    private readonly ICalculationRuleContext _context;
    
    public async Task<PensionCalculationResult> CalculateAsync(
        MemberData member, 
        List<CalculationTrigger> triggers)
    {
        // Select appropriate strategy based on member type
        var strategy = _strategies.FirstOrDefault(s => s.CanHandle(member));
        if (strategy == null)
        {
            throw new InvalidOperationException($"No calculation strategy found for member type {member.MemberType}");
        }
        
        // Build calculation context
        var context = new CalculationContext
        {
            Member = member,
            Triggers = triggers,
            EffectiveDate = triggers.Max(t => t.EffectiveDate),
            PreviousResult = await GetPreviousCalculationAsync(member.Id)
        };
        
        // Execute calculation
        var result = await strategy.CalculateAsync(context);
        
        // Validate result
        ValidateCalculation(result, context);
        
        return result;
    }
    
    private void ValidateCalculation(PensionCalculationResult result, CalculationContext context)
    {
        // Business rules validation
        if (result.MonthlyBenefit < 0)
        {
            throw new CalculationValidationException("Monthly benefit cannot be negative");
        }
        
        if (result.MonthlyBenefit > context.Member.SalaryHistory.Max() * 0.9m)
        {
            // Warning: Benefit > 90% of max salary (unusual)
            _logger.LogWarning("Unusual benefit for member {MemberId}: {Benefit}", 
                context.Member.Id, result.MonthlyBenefit);
        }
    }
}

// Event triggers cho recalculation
public class CalculationTrigger
{
    public TriggerType Type { get; set; }
    public DateTime EffectiveDate { get; set; }
    public bool IsUrgent { get; set; }
    public string TriggeredBy { get; set; } // User or System
    public Dictionary<string, object> Data { get; set; }
}

public enum TriggerType
{
    SalaryUpdate,
    ServiceYearAdded,
    RuleChange,
    MemberStatusChange,
    COLAAdjustment,
    YearEndRollover,
    ManualRecalculation
}

// Event handlers
public class MemberEventHandler
{
    private readonly IPensionPreCalculationService _preCalcService;
    
    [EventHandler]
    public async Task HandleMemberSalaryUpdatedAsync(MemberSalaryUpdatedEvent @event)
    {
        // Schedule recalculation when salary changes
        await _preCalcService.ScheduleCalculationAsync(@event.MemberId, new CalculationTrigger
        {
            Type = TriggerType.SalaryUpdate,
            EffectiveDate = @event.EffectiveDate,
            IsUrgent = false, // Can be batched
            Data = new Dictionary<string, object>
            {
                ["OldSalary"] = @event.OldSalary,
                ["NewSalary"] = @event.NewSalary
            }
        });
    }
    
    [EventHandler]
    public async Task HandleYearEndRolloverAsync(YearEndRolloverEvent @event)
    {
        // Schedule recalculation for all active members
        await _preCalcService.ScheduleBatchCalculationAsync(new CalculationBatchRequest
        {
            MemberIds = await GetAllActiveMemberIdsAsync(),
            EffectiveDate = @event.EffectiveDate,
            RunAt = @event.EffectiveDate.AddDays(-1).AddHours(2) // 2 AM
        });
    }
}

3. Optimized Data Retrieval với Batch Queries

// Batch query optimization
public class OptimizedReportDataFetcher
{
    private readonly ApplicationDbContext _context;
    
    public async Task<List<ReportDataRow>> FetchReportDataAsync(ReportGenerationRequest request)
    {
        // BAD: N+1 queries
        // var members = await _context.Members.ToListAsync();
        // foreach (var member in members)
        // {
        //     member.SalaryHistory = await _context.SalaryHistory
        //         .Where(s => s.MemberId == member.Id)
        //         .ToListAsync();
        // }
        
        // GOOD: Single query with Include
        var members = await _context.Members
            .AsNoTracking()
            .Include(m => m.SalaryHistory)
            .Include(m => m.ServiceHistory)
            .Include(m => m.BeneficiaryDesignations)
            .Where(m => m.Status == MemberStatus.Active)
            .ToListAsync();
        
        // BAD: Multiple queries in loop
        // foreach (var memberId in memberIds)
        // {
        //     var calc = await _context.Calculations
        //         .Where(c => c.MemberId == memberId)
        //         .FirstOrDefaultAsync();
        // }
        
        // GOOD: Bulk fetch
        var memberIds = members.Select(m => m.Id).ToList();
        var calculations = await _context.Calculations
            .AsNoTracking()
            .Where(c => memberIds.Contains(c.MemberId))
            .ToListAsync();
        
        var calculationLookup = calculations.ToDictionary(c => c.MemberId);
        
        // Combine data
        return members.Select(m => new ReportDataRow
        {
            MemberId = m.Id,
            MemberName = m.FullName,
            MonthlyBenefit = calculationLookup.TryGetValue(m.Id, out var calc) 
                ? calc.MonthlyBenefit 
                : 0,
            LastSalary = m.SalaryHistory.Max(s => s.SalaryAmount),
            YearsOfService = CalculateYearsOfService(m.ServiceHistory)
        }).ToList();
    }
    
    // Split large queries into chunks
    public async Task<List<ReportDataRow>> FetchLargeReportAsync(ReportGenerationRequest request)
    {
        var totalCount = await GetTotalCountAsync(request);
        var batchSize = 5000;
        var allData = new List<ReportDataRow>();
        
        for (int skip = 0; skip < totalCount; skip += batchSize)
        {
            var batch = await _context.Members
                .AsNoTracking()
                .Skip(skip)
                .Take(batchSize)
                .Select(m => new ReportDataRow
                {
                    MemberId = m.Id,
                    MemberName = m.FullName,
                    // ... projection
                })
                .ToListAsync();
            
            allData.AddRange(batch);
            
            // Log progress
            _logger.LogInformation("Fetched {Count} records, total so far: {Total}", 
                batch.Count, allData.Count);
        }
        
        return allData;
    }
}

// SqlBulkCopy for intermediate storage
public class BulkDataLoader
{
    public async Task LoadToStagingAsync(List<ReportDataRow> data, string stagingTable)
    {
        using var connection = new SqlConnection(connectionString);
        await connection.OpenAsync();
        
        using var bulkCopy = new SqlBulkCopy(connection)
        {
            DestinationTableName = stagingTable,
            BatchSize = 10000,
            BulkCopyTimeout = 300
        };
        
        // Map columns
        bulkCopy.ColumnMappings.Add("MemberId", "MemberId");
        bulkCopy.ColumnMappings.Add("MemberName", "MemberName");
        bulkCopy.ColumnMappings.Add("MonthlyBenefit", "MonthlyBenefit");
        // ...
        
        // Convert to DataTable
        var table = new DataTable();
        table.Columns.Add("MemberId", typeof(int));
        table.Columns.Add("MemberName", typeof(string));
        table.Columns.Add("MonthlyBenefit", typeof(decimal));
        // ...
        
        foreach (var row in data)
        {
            table.Rows.Add(row.MemberId, row.MemberName, row.MonthlyBenefit);
        }
        
        await bulkCopy.WriteToServerAsync(table);
    }
}

4. Report Generation với Pre-generated JSON

// Pre-generated JSON report storage
public interface IReportStorageService
{
    Task<string> GenerateAndStoreAsync(ReportGenerationRequest request);
    Task<byte[]> GetReportFileAsync(string reportId);
    Task<ReportMetadataDto> GetMetadataAsync(string reportId);
}

public class JsonReportStorageService : IReportStorageService
{
    private readonly IBlobStorageService _blobStorage;
    private readonly IReportDataSerializer _serializer;
    
    public async Task<string> GenerateAndStoreAsync(ReportGenerationRequest request)
    {
        var reportId = Guid.NewGuid().ToString();
        
        // Fetch data
        var data = await FetchReportDataAsync(request);
        
        // Serialize to JSON
        var jsonContent = _serializer.SerializeToJson(data, new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
            WriteIndented = false // Minified for smaller size
        });
        
        // Compress with GZip
        var compressedContent = CompressGZip(jsonContent);
        
        // Store in Blob Storage
        var blobName = $"reports/pre-generated/{reportId}.json.gz";
        await _blobStorage.UploadAsync(blobName, compressedContent, new Dictionary<string, string>
        {
            ["ContentType"] = "application/json",
            ["ContentEncoding"] = "gzip",
            ["ReportType"] = request.ReportType,
            ["GeneratedAt"] = DateTime.UtcNow.ToString("O")
        });
        
        // Also generate PDF for download
        var pdfBytes = await GeneratePdfFromJsonAsync(jsonContent, request);
        var pdfBlobName = $"reports/pre-generated/{reportId}.pdf";
        await _blobStorage.UploadAsync(pdfBlobName, pdfBytes);
        
        // Store metadata
        await StoreReportMetadataAsync(new ReportMetadata
        {
            Id = reportId,
            ReportType = request.ReportType,
            GeneratedAt = DateTime.UtcNow,
            BlobName = blobName,
            PdfBlobName = pdfBlobName,
            RecordCount = data.Count,
            FileSizeBytes = compressedContent.Length
        });
        
        return reportId;
    }
    
    public async Task<byte[]> GetReportFileAsync(string reportId)
    {
        var metadata = await GetMetadataAsync(reportId);
        if (metadata == null) return null;
        
        // Return pre-generated PDF
        return await _blobStorage.DownloadAsync(metadata.PdfBlobName);
    }
    
    private byte[] CompressGZip(string content)
    {
        var bytes = Encoding.UTF8.GetBytes(content);
        using var output = new MemoryStream();
        using (var gzip = new GZipStream(output, CompressionLevel.Optimal))
        using (var input = new MemoryStream(bytes))
        {
            input.CopyTo(gzip);
        }
        return output.ToArray();
    }
}

// API Controller cho async report retrieval
[ApiController]
[Route("api/[controller]")]
public class ReportsController : ControllerBase
{
    private readonly IReportGenerationService _reportService;
    private readonly IReportStorageService _storageService;
    
    [HttpPost("generate")]
    [ProducesResponseType(typeof(ReportJobDto), 202)]
    public async Task<ActionResult<ReportJobDto>> GenerateReport(
        [FromBody] ReportGenerationRequest request)
    {
        // Queue report generation (non-blocking)
        var job = await _reportService.RequestReportAsync(request);
        
        // Return 202 Accepted with job location
        return AcceptedAtAction(
            nameof(GetJobStatus),
            new { jobId = job.JobId },
            job
        );
    }
    
    [HttpGet("{jobId}/status")]
    [ProducesResponseType(typeof(ReportJobDto), 200)]
    [ProducesResponseType(404)]
    public async Task<ActionResult<ReportJobDto>> GetJobStatus(string jobId)
    {
        var job = await _reportService.GetJobStatusAsync(jobId);
        if (job == null) return NotFound();
        
        return Ok(job);
    }
    
    [HttpGet("{jobId}/download")]
    [ProducesResponseType(typeof(FileContentResult), 200)]
    [ProducesResponseType(404)]
    public async Task<IActionResult> DownloadReport(string jobId)
    {
        var job = await _reportService.GetJobStatusAsync(jobId);
        if (job == null) return NotFound();
        
        if (job.Status != JobStatus.Completed)
        {
            return BadRequest("Report is not ready yet");
        }
        
        var fileBytes = await _storageService.GetReportFileAsync(jobId);
        if (fileBytes == null) return NotFound();
        
        return File(fileBytes, "application/pdf", $"report-{jobId}.pdf");
    }
}

Thách thức & Giải pháp

Challenge 1: Transactional Outbox Pattern cho Event Consistency

Vấn đề: Đảm bảo events không bị mất khi system crash sau khi update database.

Giải pháp:

public class TransactionalOutboxService
{
    private readonly ApplicationDbContext _context;
    private readonly IEventBus _eventBus;
    
    public async Task UpdateMemberAndPublishEventAsync(
        int memberId, 
        MemberUpdateRequest request)
    {
        using var transaction = await _context.Database.BeginTransactionAsync();
        
        try
        {
            // 1. Update member data
            var member = await _context.Members.FindAsync(memberId);
            member.Update(request);
            
            // 2. Create outbox event (in same transaction)
            var outboxEvent = new OutboxEvent
            {
                Id = Guid.NewGuid(),
                EventType = "MemberUpdated",
                Payload = JsonSerializer.Serialize(new MemberUpdatedEvent
                {
                    MemberId = memberId,
                    UpdatedFields = request.GetChangedFields(),
                    UpdatedAt = DateTime.UtcNow
                }),
                Status = OutboxEventStatus.Pending,
                CreatedAt = DateTime.UtcNow
            };
            
            _context.OutboxEvents.Add(outboxEvent);
            
            // 3. Commit transaction (atomic)
            await _context.SaveChangesAsync();
            await transaction.CommitAsync();
            
            // 4. Outbox processor will publish event asynchronously
        }
        catch
        {
            await transaction.RollbackAsync();
            throw;
        }
    }
}

// Background outbox processor
public class OutboxProcessor : BackgroundService
{
    private readonly ApplicationDbContext _context;
    private readonly IEventBus _eventBus;
    
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            // Fetch pending events
            var pendingEvents = await _context.OutboxEvents
                .Where(e => e.Status == OutboxEventStatus.Pending)
                .OrderBy(e => e.CreatedAt)
                .Take(100)
                .ToListAsync();
            
            foreach (var evt in pendingEvents)
            {
                try
                {
                    // Publish event
                    var eventData = JsonSerializer.Deserialize(evt.Payload, evt.EventType);
                    await _eventBus.PublishAsync(eventData);
                    
                    // Mark as processed
                    evt.Status = OutboxEventStatus.Processed;
                    evt.ProcessedAt = DateTime.UtcNow;
                }
                catch (Exception ex)
                {
                    evt.Status = OutboxEventStatus.Failed;
                    evt.ErrorMessage = ex.Message;
                    evt.RetryCount++;
                    
                    if (evt.RetryCount >= 3)
                    {
                        // Move to dead letter after 3 retries
                        evt.Status = OutboxEventStatus.DeadLetter;
                    }
                }
            }
            
            await _context.SaveChangesAsync(stoppingToken);
            
            // Wait before next batch
            await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
        }
    }
}

Challenge 2: Handling Calculation Rule Changes

Vấn đề: Khi regulatory rules thay đổi, cần recalculate hàng ngàn members.

Giải pháp: Versioned calculation rules với effective dating.

public class CalculationRule
{
    public int Id { get; set; }
    public string RuleCode { get; set; } // e.g., "BASE_PENSION_FORMULA"
    public string RuleName { get; set; }
    public string Formula { get; set; } // e.g., "avgSalary * yearsOfService * 0.02"
    public Dictionary<string, decimal> Parameters { get; set; }
    public DateTime EffectiveFrom { get; set; }
    public DateTime? EffectiveTo { get; set; }
    public bool IsActive { get; set; }
    public string Version { get; set; }
}

public async Task<PensionCalculationResult> CalculateWithVersioningAsync(
    MemberData member, 
    DateTime asOfDate)
{
    // Get rules effective at calculation date
    var rules = await _context.CalculationRules
        .Where(r => r.RuleCode == "BASE_PENSION_FORMULA"
                 && r.EffectiveFrom <= asOfDate
                 && (r.EffectiveTo == null || r.EffectiveTo > asOfDate)
                 && r.IsActive)
        .OrderByDescending(r => r.EffectiveFrom)
        .FirstOrDefaultAsync();
    
    if (rules == null)
    {
        throw new InvalidOperationException($"No rule found effective at {asOfDate}");
    }
    
    // Execute formula with parameters
    var result = ExecuteFormula(rules.Formula, member, rules.Parameters);
    result.RuleVersion = rules.Version;
    
    return result;
}

// When rules change, schedule recalculation
public async Task HandleRuleChangeAsync(RuleChangeEvent @event)
{
    // Find affected members
    var affectedMembers = await FindAffectedMembersAsync(@event.RuleCode);
    
    // Schedule recalculation with new effective date
    await _preCalcService.ScheduleBatchCalculationAsync(new CalculationBatchRequest
    {
        MemberIds = affectedMembers.Select(id => new MemberCalculationItem { MemberId = id }).ToList(),
        EffectiveDate = @event.NewRule.EffectiveFrom,
        RunAt = @event.NewRule.EffectiveFrom.AddDays(-1).AddHours(2)
    });
}

Kết quả & Impact

Metrics

MetricBeforeAfterImprovement
Report generation time15-20s2-3s85% faster
Pension calculation time5-10s< 1s (cached)90% faster
Database CPU (peak)95%45%53% reduction
API response time (p95)2.5s0.3s88% faster
Background job success rate82%99.8%22% improvement
User satisfaction3.2/54.6/544% improvement

Business Impact

  • 50% reduction in report generation time
  • 90% faster pension calculations
  • 60% reduction in support tickets related to slow performance
  • Zero data loss với transactional outbox pattern
  • 100% audit compliance với full calculation history

Bài học kinh nghiệm

Technical Learnings

  1. Pre-calculation is key: Don’t calculate on-the-fly cho complex business logic
  2. Event-driven architecture: Decouple components với message queues
  3. Batch processing: Group operations để giảm database round trips
  4. Caching strategy: Multi-level caching (memory, distributed, database)
  5. Observability: Instrument mọi thứ từ đầu (metrics, logs, traces)

Architecture Learnings

  1. Transactional Outbox: Đảm bảo event consistency
  2. Saga Pattern: Handle distributed transactions
  3. CQRS: Separate read/write models cho optimization
  4. Versioning: Rule versioning cho regulatory changes

Soft Skills

  1. Working in large team: Clear APIs, documentation, code reviews
  2. Stakeholder communication: Translate technical to business value
  3. Incremental improvement: Optimize gradually, measure impact

Câu hỏi phỏng vấn

Q1: Bạn đã design background job architecture như thế nào?

A:

┌─────────────┐    ┌──────────────┐    ┌──────────────┐
│  API Layer  │ →  │  Job Queue   │ →  │  Job Worker  │
│  (Request)  │    │  (Service    │    │  (Hangfire)  │
│             │    │   Bus)       │    │              │
└─────────────┘    └──────────────┘    └──────────────┘
                          │                    │
                          ▼                    ▼
                   ┌──────────────┐    ┌──────────────┐
                   │  Job Status  │    │  Progress    │
                   │  Tracking    │    │  Updates     │
                   └──────────────┘    └──────────────┘

Key design decisions:

  • Priority queues: Critical (payment), High (regulatory), Default, Background
  • Retry policy: Exponential backoff với max 3 retries
  • Idempotency: Job deduplication với unique job keys
  • Monitoring: Application Insights + custom dashboards

Q2: Làm sao để đảm bảo calculation accuracy 100%?

A:

  1. Unit tests: Test từng calculation component với known inputs/outputs
  2. Integration tests: End-to-end tests với test data sets
  3. Parallel run: Run new system alongside old system, compare results
  4. Audit trail: Lưu full calculation history với input snapshots
  5. Reconciliation: Daily reconciliation reports
  6. Manual review: Flag unusual results (> 2 std dev from mean)
// Calculation audit log
public class CalculationAuditLog
{
    public int MemberId { get; set; }
    public DateTime CalculationDate { get; set; }
    public string RuleVersion { get; set; }
    public Dictionary<string, object> InputSnapshot { get; set; }
    public Dictionary<string, decimal> ResultBreakdown { get; set; }
    public decimal FinalBenefit { get; set; }
    public string CalculatedBy { get; set; } // User or System
    public string ReviewStatus { get; set; } // AutoApproved, ManualReview, Rejected
    public string ReviewNotes { get; set; }
}

Q3: Bạn xử lý concurrent updates như thế nào?

A:

// Optimistic concurrency với row version
public class Member
{
    public int Id { get; set; }
    public string Name { get; set; }
    // ...
    
    [Timestamp]
    public byte[] RowVersion { get; set; }
}

public async Task UpdateMemberAsync(int memberId, MemberUpdateRequest request)
{
    var member = await _context.Members.FindAsync(memberId);
    if (member == null) return;
    
    // Apply changes
    member.Update(request);
    
    try
    {
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException ex)
    {
        // Concurrency conflict
        _logger.LogWarning("Concurrency conflict for member {MemberId}", memberId);
        
        // Option 1: Reload and retry
        var entry = ex.Entries.Single();
        var databaseValues = await entry.GetDatabaseValuesAsync();
        
        if (databaseValues == null)
        {
            throw new NotFoundException($"Member {memberId} not found");
        }
        
        // Refresh original values
        entry.OriginalValues.SetValues(databaseValues);
        
        // Retry
        await _context.SaveChangesAsync();
        
        // Option 2: Return conflict to user
        // throw new ConcurrencyException("Record was modified by another user");
    }
}

Q4: Trade-offs giữa Pre-calculation vs On-the-fly calculation?

A:

AspectPre-calculationOn-the-fly
Read latencyVery fast (cache hit)Slow
Write latencyHigher (trigger calc)Lower
Data freshnessEventually consistentAlways current
StorageHigher (store results)Lower
ComplexityHigher (job scheduling)Lower
Best forRead-heavy, complex calcWrite-heavy, simple calc

Decision for Pension System:

  • Read-heavy: Users query benefits frequently
  • Complex calculation: Many factors, business rules
  • Acceptable staleness: Results valid until next event → Pre-calculation wins

Q5: Nếu phải redesign lại system, bạn sẽ thay đổi gì?

A:

  1. Event Sourcing: Store events instead of current state → full audit trail, temporal queries
  2. GraphQL API: More flexible queries cho frontend
  3. Microservices: Split by bounded contexts (Member, Calculation, Payment)
  4. Data Lake: Store historical data cho analytics và ML
  5. Better testing: More property-based tests cho calculation logic
  6. Feature flags: Safer rollouts cho rule changes

← SKCC Project - FPT | Xem Interview Q&A →

Interview Questions & Answers

Bộ câu hỏi phỏng vấn comprehensive dựa trên kinh nghiệm thực tế từ các dự án.


Mục lục

  1. Technical Questions by Project
  2. System Design Questions
  3. Backend Deep Dive
  4. Behavioral Questions
  5. Salary Negotiation

1. Technical Questions by Project

KF Project - Optimizely CMS

Q: Tích hợp Google Maps vào CMS như thế nào?

Context: Đây là câu hỏi core về dự án KF Project.

Câu trả lời mẫu (STAR method):

S (Situation): 
"Khách hàng cần CMS cho phép content editors quản lý locations với map visualization."

T (Task): 
"Tôi được giao thiết kế và implement Google Maps integration với yêu cầu:
- Editors có thể chọn location trên map
- Lưu tọa độ vào CMS
- Hiển thị store locator cho end users"

A (Action): 
"Tôi đã implement solution với các thành phần:

1. Backend API (.NET Core):
   - REST endpoints cho CRUD locations
   - Geocoding service với caching
   - Spatial queries với SQL Server

2. Frontend (React):
   - Google Maps React component
   - Interactive marker placement
   - Viewport-based lazy loading

3. Optimization:
   - Redis cache cho geocoding results
   - Batching cho bulk imports
   - CDN cho map tiles"

R (Result): 
"Kết quả:
- Page load time giảm từ 4.5s → 1.8s (60% faster)
- Map render time giảm từ 2.1s → 0.4s (81% faster)
- Content editors tiết kiệm 70% thời gian quản lý locations"

Code snippet để share (nếu có whiteboard):

// Location Service với caching
public class CachedLocationService
{
    public async Task<LocationDto> GetLocationAsync(int id)
    {
        var cacheKey = $"location:{id}";
        
        // Try cache first
        var cached = await _cache.GetAsync<LocationDto>(cacheKey);
        if (cached != null) return cached;
        
        // Cache miss - query database
        var location = await _repository.GetByIdAsync(id);
        
        // Cache for 30 minutes
        await _cache.SetAsync(cacheKey, location, TimeSpan.FromMinutes(30));
        
        return location;
    }
}

Q: Bạn xử lý concurrency trong CMS như thế nào?

A:

"Tôi implement optimistic concurrency control với ETags:

1. Khi client GET resource, server trả về ETag header
2. Khi client PUT/PATCH, gửi ETag trong If-Match header
3. Server so sánh ETag với current version
4. Nếu mismatch → trả về 409 Conflict

Implementation:
- Optimizely tự động quản lý content versions
- Custom ETag calculation dựa trên content hash
- Conflict resolution UI cho editors

Kết quả: Zero data loss từ concurrent edits."

SKCC Project - Industrial Monitoring

Q: Thiết kế hệ thống real-time monitoring với hàng ngàn sensors?

Câu trả lời:

S: "Nhà máy cần monitoring system cho 5000+ sensors với update frequency 1-5 giây."

T: "Design system với yêu cầu:
- End-to-end latency < 2s
- Xử lý 10,000+ events/giây
- Real-time visualization
- Alert khi vượt thresholds"

A: "Architecture:

1. Data Ingestion:
   - .NET Core service nhận sensor data
   - Validate và transform data
   - Publish events lên Azure Service Bus

2. Data Storage:
   - ElasticSearch cho time-series data
   - Index rollover monthly
   - SQL Server cho metadata

3. Real-time Processing:
   - Alert service consume events
   - Check thresholds với rule engine
   - Trigger notifications qua webhooks

4. Visualization:
   - WPF client với SignalR connection
   - Chart downsampling (LTTB algorithm)
   - Data virtualization cho performance

5. Optimization:
   - Batch processing với Channel<T>
   - Connection pooling cho ElasticSearch
   - Circuit breaker cho webhook calls"

R: "Metrics:
- Event processing latency: 5.2s → 0.8s (85% faster)
- Chart render time: 3.5s → 0.3s (91% faster)
- Webhook success rate: 78% → 99.5%"

Q: ElasticSearch optimization techniques bạn đã dùng?

A:

"Tôi đã apply các optimization techniques sau:

1. Index Design:
   - Index templates với proper mappings
   - Keyword fields cho exact match
   - Disable _source cho fields không cần

2. ILM (Index Lifecycle Management):
   - Hot phase: 30 days, 3 shards
   - Warm phase: Shrink to 1 shard
   - Delete after 2 years

3. Query Optimization:
   - Use filter context thay vì query context
   - search_after cho deep pagination
   - Aggregations thay vì fetch all docs

4. Write Performance:
   - Bulk indexing với batch size 1000
   - Refresh interval 5s (default 1s)
   - Translog async

5. Read Performance:
   - Request cache cho frequent queries
   - Shard allocation awareness
   - Force merge cho old indices"

PTG.PPPlus3 - Pension System

Q: Bạn đã optimize report generation như thế nào?

A:

S: "Report generation mất 15-20 giây, users phải wait synchronous."

T: "Mục tiêu: Giảm xuống < 3s, improve user experience."

A: "Solution với 3 phases:

Phase 1 - Background Jobs:
- Move report generation khỏi request pipeline
- Azure Service Bus queues cho job scheduling
- SignalR cho real-time progress updates
- Users receive notification khi report ready

Phase 2 - Data Optimization:
- Batch queries thay vì N+1
- Include() để eager load
- Split large queries into chunks
- SqlBulkCopy cho intermediate storage

Phase 3 - Pre-generation:
- Generate JSON reports asynchronously
- Compress với GZip
- Store trong Azure Blob Storage
- Serve static files thay vì dynamic generation

Additional:
- Priority queues cho critical reports
- Retry policy với exponential backoff
- Dead letter queue cho failed jobs"

R: "Results:
- Report time: 15-20s → 2-3s (85% faster)
- Database CPU: 95% → 45% (53% reduction)
- User satisfaction: 3.2/5 → 4.6/5"

Q: Pre-calculation strategy cho pension calculations?

A:

"Problem: Calculation phức tạp mất 5-10 giây cho mỗi member.

Solution - Event-driven Pre-calculation:

1. Trigger Events:
   - Salary update
   - Service year added
   - Rule changes
   - Year-end rollover

2. Calculation Scheduling:
   - Urgent events: Calculate within 5 minutes
   - Non-urgent: Batch at off-peak hours (2 AM)
   - Bulk recalculation: For rule changes

3. Caching:
   - Redis cache cho latest results
   - Cache TTL: 24 hours
   - Cache invalidation on new calculation

4. Storage:
   - Save calculation results với breakdown
   - Store input snapshot cho audit
   - Version control cho calculation rules

5. Consistency:
   - Transactional outbox pattern
   - Event sourcing cho audit trail
   - Idempotent calculation jobs

Result: Calculation time 5-10s → < 1s (cache hit)"

2. System Design Questions

Q: Design a URL Shortener

Gợi ý trả lời:

1. Requirements:
   - Functional: Shorten URL, redirect, analytics
   - Non-functional: High availability, low latency

2. Estimation:
   - 500M URLs/month → 200 RPS create, 20k RPS redirect
   - Storage: 250 GB/month

3. High-level Design:
   [Client] → [LB] → [App Server] → [DB]
                         ↓
                      [Cache]

4. Key Components:
   - Encoding: Base62 (62^7 combinations)
   - Storage: NoSQL với sharding
   - Cache: Redis cho popular URLs
   - Redirect: 301 permanent

5. Trade-offs:
   - Counter vs Hash encoding
   - SQL vs NoSQL storage
   - Cache consistency model

Q: Design a Rate Limiter

Gợi ý trả lời:

1. Requirements:
   - Limit 100 requests/minute per user
   - Distributed system

2. Algorithms:
   - Token Bucket: Flexible, allows bursts
   - Sliding Window: Accurate but complex
   - Fixed Window: Simple but boundary issues

3. Implementation với Redis:
```csharp
public class RateLimiter
{
    public async Task<bool> AllowRequestAsync(string userId, int limit)
    {
        var key = $"ratelimit:{userId}";
        var window = TimeSpan.FromMinutes(1);
        
        // Atomic increment với expiry
        var count = await _redis.StringIncrementAsync(key);
        if (count == 1)
        {
            await _redis.KeyExpireAsync(key, window);
        }
        
        return count <= limit;
    }
}
  1. Distributed Considerations:
    • Redis cluster cho scaling
    • Sync rate limits across regions
    • Handle Redis failures gracefully

### Q: Design a Background Job System

**Gợi ý trả lời**:
  1. Requirements:

    • Schedule and process jobs
    • Retry failed jobs
    • Priority queues
    • Progress tracking
  2. Architecture: [API] → [Job Queue] → [Workers] ↓ ↓ [Status DB] [Job Store]

  3. Key Components:

    • Queue: Azure Service Bus / RabbitMQ
    • Scheduler: Hangfire / Quartz.NET
    • Storage: SQL Server cho persistence
    • Monitoring: Dashboard + alerts
  4. Reliability:

    • Idempotent jobs
    • Exponential backoff retry
    • Dead letter queue
    • Circuit breaker pattern

---

## 3. Backend Deep Dive

### Message Queue Patterns

#### Q: Khi nào dùng Queue vs Topic?

**A**:

Queue (Point-to-Point):

  • Single consumer per message
  • Use case: Task distribution, load leveling
  • Example: Report generation jobs

Topic (Pub/Sub):

  • Multiple subscribers per message
  • Use case: Event broadcasting
  • Example: Member updated → multiple services react

Trong Pension System:

  • Queue: Report jobs (one worker processes each)
  • Topic: Domain events (Calculation, Payment, Audit all listen)

---

#### Q: Xử lý message failures như thế nào?

**A**:
  1. Retry Policy:

    • Immediate retry: 3 attempts với delay
    • Exponential backoff: 1s, 10s, 60s
    • Max retries: 3-5 tùy criticality
  2. Dead Letter Queue:

    • Move failed messages sau max retries
    • Manual inspection và reprocessing
    • Alert on DLQ size threshold
  3. Idempotency:

    • Unique message ID
    • Check processed messages before handling
    • Deduplication window (24 hours)
  4. Monitoring:

    • Track failure rate per queue
    • Alert on sudden spikes
    • Dashboard với queue depths

---

### Caching Strategies

#### Q: Cache invalidation strategies?

**A**:
  1. Time-based (TTL):

    • Simple, automatic
    • Risk: Stale data within TTL
    • Use: Frequently changing data
  2. Event-based:

    • Invalidate on data change
    • More accurate
    • Use: Critical data consistency
  3. Write-through:

    • Update cache and DB together
    • Strong consistency
    • Use: Read-heavy, write-rarely
  4. Cache-aside (Lazy loading):

    • Load on cache miss
    • Simple implementation
    • Use: General purpose

Trong projects:

  • Optimizely: TTL 30 minutes cho locations
  • Pension System: Event-based invalidation cho calculations

---

### Database Optimization

#### Q: Query optimization techniques?

**A**:
  1. Indexing:

    • Covering indexes (INCLUDE columns)
    • Composite indexes (order matters!)
    • Filtered indexes for subsets
  2. Query Patterns:

    • Avoid SELECT *
    • Use EXISTS thay vì COUNT for existence checks
    • Parameterized queries (plan caching)
  3. Execution Plans:

    • Look for table scans → add indexes
    • Check join types (Nested Loop vs Hash Match)
    • Statistics updates
  4. Specific Examples:

-- Bad: N+1 query
SELECT * FROM Members;
-- Then for each member: SELECT * FROM SalaryHistory WHERE MemberId = ?

-- Good: Single query with JOIN
SELECT m.*, sh.*
FROM Members m
LEFT JOIN SalaryHistory sh ON m.Id = sh.MemberId
WHERE m.Status = 'Active';

-- Bad: Function on indexed column
WHERE YEAR(CreatedAt) = 2024

-- Good: SARGable
WHERE CreatedAt >= '2024-01-01' AND CreatedAt < '2025-01-01'

4. Behavioral Questions

Q: Tell me about a challenging technical problem you solved

Câu trả lời mẫu:

"The most challenging problem was optimizing the pension calculation system.

Challenge:
- Calculations took 5-10 seconds per member
- With 500,000 members, batch processing took days
- Users experienced slow UI when viewing benefits

My Approach:
1. First, I profiled the code to identify bottlenecks
   - Found 60% time in database queries
   - 30% in calculation logic
   - 10% in object mapping

2. Then I implemented a multi-phase solution:
   - Phase 1: Pre-calculation with event-driven updates
   - Phase 2: Multi-level caching (Redis + in-memory)
   - Phase 3: Batch processing optimization

3. Results:
   - Calculation time reduced by 90%
   - Database load reduced by 53%
   - User satisfaction improved from 3.2 to 4.6

Key Learnings:
- Always measure before optimizing
- Pre-computation is powerful for read-heavy workloads
- Communication with stakeholders about trade-offs is crucial"

Q: Describe a time you had a disagreement with your team

Câu trả lời mẫu:

"During the SKCC project, we had a debate about choosing ElasticSearch vs SQL Server for sensor data.

Situation:
- Team was split: half wanted SQL Server (familiar, ACID)
- Half (including me) advocated for ElasticSearch (time-series optimization)

My Approach:
1. I proposed a proof-of-concept to compare both
2. Created test datasets with 10M records
3. Ran benchmark queries for typical use cases

Results:
- ElasticSearch was 10x faster for time-range queries
- ElasticSearch had better aggregation performance
- SQL Server had better transaction support

Outcome:
- We chose ElasticSearch for sensor data, SQL Server for metadata
- The POC data convinced the skeptical team members
- Learned that data-driven discussions are more effective than opinions"

Q: How do you handle tight deadlines?

Câu trả lời mẫu:

"In the KF Project, we had a critical deadline for a client demo.

Situation:
- Demo was in 2 weeks
- Google Maps integration was only 50% complete
- Team was concerned about meeting deadline

My Approach:
1. Broke down remaining work into smallest tasks
2. Identified must-have vs nice-to-have features
3. Proposed MVP scope for demo, full features later
4. Worked with frontend dev in pair programming mode
5. Daily check-ins with stakeholder on progress

Actions:
- Focused on core flow: select location → save → display
- Deferred advanced features (clustering, bulk import)
- Used existing libraries instead of custom code
- Automated testing for critical paths

Result:
- Delivered MVP 2 days before demo
- Demo was successful, client approved
- Full features delivered 2 weeks later

Learning:
- Clear communication about scope trade-offs
- Focus on user value, not perfection"

5. Salary Negotiation

Research & Preparation

1. Market Research:
   - Check Glassdoor, Levels.fyi for role/level
   - Talk to recruiters about market rates
   - Consider location, company size, industry

2. Know Your Value:
   - List specific achievements with metrics
   - Quantify impact (revenue, cost savings, efficiency)
   - Unique skills you bring

3. Define Your Range:
   - Minimum acceptable (walk-away number)
   - Target (fair market value)
   - Stretch (optimistic scenario)

Negotiation Scripts

When asked about current salary:
"I'd prefer to focus on the value I can bring to this role. 
Based on my research and experience, I'm looking for a range of X-Y."

When given an offer below expectations:
"Thank you for the offer. I'm excited about the opportunity.
However, based on my experience with [specific achievements] 
and market research, I was expecting something closer to X.
Is there flexibility in the offer?"

When they ask about your expectations:
"Based on my research for similar roles in this market, 
and considering my experience with [relevant skills], 
I'm looking for a total compensation in the range of X-Y.
Of course, I'm open to discussing the full package."

Key Principles

1. Don't be the first to mention a number
2. Anchor high (but reasonable)
3. Negotiate total compensation, not just base salary
4. Get everything in writing
5. Be prepared to walk away
6. Maintain positive relationship throughout

6. Questions to Ask Interviewers

Technical Questions

  • “What does your typical deployment pipeline look like?”
  • “How do you handle technical debt?”
  • “What’s your approach to code reviews?”
  • “How do you monitor production systems?”

Team & Culture

  • “How is the team structured?”
  • “What does a typical sprint look like?”
  • “How do you handle knowledge sharing?”
  • “What opportunities are there for learning and growth?”

Project-Specific

  • “What are the biggest technical challenges the team is facing?”
  • “What does success look like in the first 90 days?”
  • “How do you prioritize technical work vs feature requests?”

7. Final Tips

Before Interview

  • Review your projects’ key metrics
  • Prepare 3-5 stories using STAR method
  • Practice coding problems (LeetCode, HackerRank)
  • Research company and role

During Interview

  • Think aloud when solving problems
  • Ask clarifying questions
  • Admit when you don’t know something
  • Show enthusiasm and curiosity

After Interview

  • Send thank-you email within 24 hours
  • Reflect on what went well / could improve
  • Follow up if you haven’t heard back

← PTG.PPPlus3 Project | Quay lại Work Experience

Thuật toán & Giải thuật

Bộ sưu tập các bài toán lập trình经典 với lời giải chi tiết bằng C#, tuân thủ các nguyên tắc SOLID, CLEAN, DRY và có thể mở rộng.


Mục tiêu

  • Code chất lượng: Clean code, SOLID principles, proper error handling
  • Testability: Dễ dàng unit test, dependency injection
  • Reusability: Code có thể reuse trong các ngữ cảnh khác
  • Scalability: Giải pháp có thể mở rộng cho variants phức tạp hơn
  • Performance: Phân tích time/space complexity

Danh sách bài toán

1. Bài toán cơ bản

#Tên bàiĐộ khóKhái niệm chính
1Giải phương trình bậc nhấtEasyException handling, validation, result pattern
2Best Time to Buy/Sell StockEasySingle pass, greedy algorithm

Cấu trúc mỗi bài giải

Mỗi bài toán được trình bày theo cấu trúc:

  1. Đề bài: Mô tả yêu cầu, input/output
  2. Phân tích: Requirements, edge cases, constraints
  3. Giải pháp: Code với giải thích chi tiết
  4. Unit Tests: Test cases đầy đủ
  5. Mở rộng: Variants phức tạp hơn của bài toán
  6. Giải pháp mở rộng: Code cho variants

Nguyên tắc coding áp dụng

SOLID Principles

S - Single Responsibility: Mỗi class/method một trách nhiệm
O - Open/Closed: Mở cho extension, đóng cho modification
L - Liskov Substitution: Derived classes thay thế được base classes
I - Interface Segregation: Interfaces nhỏ, specific
D - Dependency Inversion: Depend vào abstractions

CLEAN Code

- Đặt tên có ý nghĩa (meaningful names)
- Functions nhỏ, làm một việc
- Comments tối thiểu, code tự giải thích
- Error handling rõ ràng
- No magic numbers

DRY (Don’t Repeat Yourself)

- Extract common logic
- Use helper methods
- Generic solutions when applicable

Các khái niệm thường dùng

Result Pattern

Thay vì ném exception, trả về result object:

public class Result<T>
{
    public bool IsSuccess { get; }
    public T Value { get; }
    public string Error { get; }
    
    private Result(bool isSuccess, T value, string error)
    {
        IsSuccess = isSuccess;
        Value = value;
        Error = error;
    }
    
    public static Result<T> Success(T value) 
        => new Result<T>(true, value, null);
    
    public static Result<T> Failure(string error) 
        => new Result<T>(false, default, error);
}

Guard Clauses

public static class Guard
{
    public static void AgainstNull<T>(T value, string paramName) 
        where T : class
    {
        if (value == null)
            throw new ArgumentNullException(paramName);
    }
    
    public static void AgainstNullOrEmpty(string value, string paramName)
    {
        if (string.IsNullOrEmpty(value))
            throw new ArgumentException("Value cannot be empty", paramName);
    }
    
    public static void AgainstNullOrEmpty<T>(IEnumerable<T> value, string paramName)
    {
        if (value == null || !value.Any())
            throw new ArgumentException("Collection cannot be empty", paramName);
    }
}

Tài liệu tham khảo


← Work Experience | Xem bài đầu tiên →

Bài 1: Giải phương trình bậc nhất ax + b = 0

Đề bài

Cho phương trình bậc nhất 1 ẩn: ax + b = 0

Viết hàm giải phương trình với yêu cầu:

  • Code tối ưu, chặt chẽ
  • Xử lý exception hợp lý
  • Có thể reuse
  • Dễ dàng unit test
  • Không dùng third-party libraries
  • Ngôn ngữ: C#

Phân tích

Các trường hợp

1. a ≠ 0 → nghiệm duy nhất x = -b/a
2. a = 0, b = 0 → vô số nghiệm
3. a = 0, b ≠ 0 → vô nghiệm

Edge cases cần xử lý

- a rất gần 0 (floating point precision)
- a hoặc b là NaN, Infinity
- Negative values (valid)
- Overflow khi a rất nhỏ

Solution chính: Double Input

Final Version

public static class LinearEquationSolver
{
    private const double EPSILON = 1e-10;
    
    /// <summary>
    /// Giải phương trình bậc nhất ax + b = 0
    /// </summary>
    /// <returns>Nghiệm x (double)</returns>
    /// <exception cref="ArgumentException">a hoặc b là NaN/Infinity</exception>
    /// <exception cref="InvalidOperationException">Vô nghiệm hoặc vô số nghiệm</exception>
    public static double Solve(double a, double b)
    {
        // Validate input
        if (double.IsNaN(a) || double.IsNaN(b))
            throw new ArgumentException("Input cannot be NaN");
        
        if (double.IsInfinity(a) || double.IsInfinity(b))
            throw new ArgumentException("Input cannot be Infinity");
        
        // Check if a ≠ 0 (với floating point precision)
        if (Math.Abs(a) > EPSILON)
        {
            return -b / a;
        }
        
        // a = 0
        if (Math.Abs(b) < EPSILON)
            throw new InvalidOperationException("Phương trình có vô số nghiệm");
        
        throw new InvalidOperationException("Phương trình vô nghiệm");
    }
    
    /// <summary>
    /// Try solve - không throw exception cho no solution/infinite solutions
    /// </summary>
    public static bool TrySolve(double a, double b, out double? solution)
    {
        // Validate input - vẫn throw cho invalid input
        if (double.IsNaN(a) || double.IsNaN(b))
            throw new ArgumentException("Input cannot be NaN");
        
        if (double.IsInfinity(a) || double.IsInfinity(b))
            throw new ArgumentException("Input cannot be Infinity");
        
        if (Math.Abs(a) > EPSILON)
        {
            solution = -b / a;
            return true;
        }
        
        // a = 0 → vô nghiệm hoặc vô số nghiệm
        solution = null;
        return false;
    }
}

Usage

// Case 1: Nghiệm duy nhất
double x = LinearEquationSolver.Solve(2.5, -5); // x = 2.0
Console.WriteLine($"x = {x}");

// Case 2: Nghiệm thập phân
x = LinearEquationSolver.Solve(3, 1); // x = 0.333...
Console.WriteLine($"x = {x:F10}"); // 0.3333333333

// Case 3: Vô nghiệm
try
{
    LinearEquationSolver.Solve(0, 5);
}
catch (InvalidOperationException ex)
{
    Console.WriteLine(ex.Message); // "Phương trình vô nghiệm"
}

// Case 4: Dùng TrySolve
if (LinearEquationSolver.TrySolve(2.5, -5, out var solution))
{
    Console.WriteLine($"Nghiệm: {solution}");
}
else
{
    Console.WriteLine("Không có nghiệm duy nhất");
}

// Case 5: Invalid input
try
{
    LinearEquationSolver.Solve(double.NaN, 5);
}
catch (ArgumentException ex)
{
    Console.WriteLine(ex.Message); // "Input cannot be NaN"
}

Mở rộng 1: Fraction Result (Nghiệm phân số)

Problem

Khi a, b là số nguyên, nghiệm -b/a có thể biểu diễn chính xác dưới dạng phân số.
Ví dụ: 3x + 1 = 0 → x = -1/3 (chính xác hơn 0.333...)

Solution

/// <summary>
/// Biểu diễn phân số tối giản
/// </summary>
public readonly struct Fraction
{
    public int Numerator { get; }    // Tử số
    public int Denominator { get; }  // Mẫu số (luôn dương)
    
    public Fraction(int numerator, int denominator)
    {
        if (denominator == 0)
            throw new ArgumentException("Denominator cannot be zero");
        
        if (denominator < 0)
        {
            numerator = -numerator;
            denominator = -denominator;
        }
        
        // Rút gọn phân số
        int gcd = GCD(Math.Abs(numerator), denominator);
        Numerator = numerator / gcd;
        Denominator = denominator / gcd;
    }
    
    /// <summary>
    /// Chuyển sang decimal (có làm tròn)
    /// </summary>
    public double ToDouble() => (double)Numerator / Denominator;
    
    /// <summary>
    /// Kiểm tra có phải số nguyên không
    /// </summary>
    public bool IsInteger => Denominator == 1;
    
    public override string ToString()
    {
        if (Denominator == 1)
            return Numerator.ToString();
        return $"{Numerator}/{Denominator}";
    }
    
    public override bool Equals(object obj)
    {
        if (obj is Fraction other)
        {
            return Numerator == other.Numerator && 
                   Denominator == other.Denominator;
        }
        return false;
    }
    
    public override int GetHashCode() => 
        HashCode.Combine(Numerator, Denominator);
    
    /// <summary>
    /// Tìm ước chung lớn nhất
    /// </summary>
    private static int GCD(int a, int b)
    {
        while (b != 0)
        {
            int temp = b;
            b = a % b;
            a = temp;
        }
        return a;
    }
}

/// <summary>
/// Solver trả về kết quả dạng phân số
/// </summary>
public static class FractionEquationSolver
{
    private const double EPSILON = 1e-10;
    
    /// <summary>
    /// Giải phương trình và trả về phân số (chính xác)
    /// </summary>
    public static Fraction Solve(double a, double b)
    {
        if (double.IsNaN(a) || double.IsNaN(b))
            throw new ArgumentException("Input cannot be NaN");
        
        if (double.IsInfinity(a) || double.IsInfinity(b))
            throw new ArgumentException("Input cannot be Infinity");
        
        if (Math.Abs(a) > EPSILON)
        {
            // Làm tròn để lấy tử/mẫu nguyên
            int numerator = (int)Math.Round(-b);
            int denominator = (int)Math.Round(a);
            return new Fraction(numerator, denominator);
        }
        
        if (Math.Abs(b) < EPSILON)
            throw new InvalidOperationException("Vô số nghiệm");
        
        throw new InvalidOperationException("Vô nghiệm");
    }
}

Usage

// 3x + 1 = 0 → x = -1/3
Fraction x = FractionEquationSolver.Solve(3, 1);
Console.WriteLine($"x = {x}"); // x = -1/3
Console.WriteLine($"x = {x.ToDouble():F10}"); // 0.3333333333

// 2x - 4 = 0 → x = 2 (nguyên)
x = FractionEquationSolver.Solve(2, -4);
Console.WriteLine($"x = {x}"); // x = 2
Console.WriteLine($"Is integer: {x.IsInteger}"); // True

// 6x + 3 = 0 → x = -1/2 (rút gọn từ -3/6)
x = FractionEquationSolver.Solve(6, 3);
Console.WriteLine($"x = {x}"); // x = -1/2

Mở rộng 2: Phương trình bậc 2

ax² + bx + c = 0

/// <summary>
/// Kết quả giải phương trình bậc 2
/// </summary>
/// <param name="HasSolutions">Có nghiệm thực không</param>
/// <param name="X1">Nghiệm thứ nhất (nhỏ hơn)</param>
/// <param name="X2">Nghiệm thứ hai (lớn hơn, null nếu nghiệm kép)</param>
public readonly record struct QuadraticResult(
    bool HasSolutions,
    double X1,
    double? X2
)
{
    /// <summary>
    /// Số nghiệm thực (0, 1, hoặc 2)
    /// </summary>
    public int SolutionCount => !HasSolutions ? 0 : (X2.HasValue ? 2 : 1);
    
    /// <summary>
    /// Nghiệm kép (nếu có)
    /// </summary>
    public double? RepeatedRoot => X2.HasValue ? null : X1;
}

public static class QuadraticEquationSolver
{
    private const double EPSILON = 1e-10;
    
    /// <summary>
    /// Giải phương trình bậc 2: ax² + bx + c = 0
    /// </summary>
    /// <returns>
    /// QuadraticResult với:
    /// - HasSolutions = false: vô nghiệm thực
    /// - HasSolutions = true, X2 = null: nghiệm kép X1
    /// - HasSolutions = true, X2.HasValue: 2 nghiệm phân biệt X1, X2
    /// </returns>
    public static QuadraticResult Solve(double a, double b, double c)
    {
        // Validate
        if (double.IsNaN(a) || double.IsNaN(b) || double.IsNaN(c))
            throw new ArgumentException("Input cannot be NaN");
        
        if (double.IsInfinity(a) || double.IsInfinity(b) || double.IsInfinity(c))
            throw new ArgumentException("Input cannot be Infinity");
        
        // a = 0 → suy biến thành bậc nhất
        if (Math.Abs(a) < EPSILON)
        {
            double x = LinearEquationSolver.Solve(b, c);
            return new QuadraticResult(HasSolutions: true, X1: x, X2: null);
        }
        
        // Tính delta
        double delta = b * b - 4 * a * c;
        
        if (delta > EPSILON)
        {
            // 2 nghiệm phân biệt
            double sqrtDelta = Math.Sqrt(delta);
            double x1 = (-b - sqrtDelta) / (2 * a);
            double x2 = (-b + sqrtDelta) / (2 * a);
            
            // Đảm bảo x1 < x2
            if (x1 > x2)
                (x1, x2) = (x2, x1);
            
            return new QuadraticResult(HasSolutions: true, X1: x1, X2: x2);
        }
        else if (Math.Abs(delta) < EPSILON)
        {
            // 1 nghiệm kép
            double x = -b / (2 * a);
            return new QuadraticResult(HasSolutions: true, X1: x, X2: null);
        }
        else
        {
            // Vô nghiệm thực
            return new QuadraticResult(HasSolutions: false, X1: 0, X2: null);
        }
    }
    
    /// <summary>
    /// Try solve - trả về QuadraticResult, không throw exception
    /// </summary>
    public static QuadraticResult TrySolve(double a, double b, double c)
    {
        try
        {
            return Solve(a, b, c);
        }
        catch
        {
            return new QuadraticResult(HasSolutions: false, X1: 0, X2: null);
        }
    }
}

Usage

// 2 nghiệm phân biệt: x² - 5x + 6 = 0 → x = 2, 3
var result = QuadraticEquationSolver.Solve(1, -5, 6);
Console.WriteLine($"Số nghiệm: {result.SolutionCount}"); // 2
Console.WriteLine($"x1 = {result.X1}, x2 = {result.X2}"); // x1 = 2, x2 = 3

// 1 nghiệm kép: x² - 4x + 4 = 0 → x = 2
result = QuadraticEquationSolver.Solve(1, -4, 4);
Console.WriteLine($"Nghiệm kép: {result.RepeatedRoot}"); // 2
Console.WriteLine($"x1 = {result.X1}, x2 = {result.X2}"); // x1 = 2, x2 = null

// Vô nghiệm: x² + x + 1 = 0
result = QuadraticEquationSolver.Solve(1, 1, 1);
Console.WriteLine($"Có nghiệm: {result.HasSolutions}"); // False

// Pattern matching
var (hasSolutions, x1, x2) = QuadraticEquationSolver.Solve(1, -5, 6);
if (hasSolutions)
{
    if (x2.HasValue)
        Console.WriteLine($"2 nghiệm: {x1} và {x2.Value}");
    else
        Console.WriteLine($"Nghiệm kép: {x1}");
}
else
{
    Console.WriteLine("Vô nghiệm");
}

// Suy biến thành bậc nhất: 0x² + 2x - 4 = 0 → x = 2
result = QuadraticEquationSolver.Solve(0, 2, -4);
Console.WriteLine($"x = {result.X1}"); // 2

Unit Tests

using Xunit;

public class LinearEquationSolverTests
{
    [Fact]
    public void Solve_NonZeroA_ReturnsCorrectSolution()
    {
        // 2.5x - 5 = 0 → x = 2
        double x = LinearEquationSolver.Solve(2.5, -5);
        Assert.Equal(2.0, x);
    }
    
    [Fact]
    public void Solve_NegativeCoefficients_ReturnsCorrectSolution()
    {
        // -3x + 9 = 0 → x = 3
        double x = LinearEquationSolver.Solve(-3, 9);
        Assert.Equal(3.0, x);
    }
    
    [Fact]
    public void Solve_FractionalSolution_ReturnsDecimal()
    {
        // 3x + 1 = 0 → x = -1/3
        double x = LinearEquationSolver.Solve(3, 1);
        Assert.Equal(-1.0 / 3.0, x, 10);
    }
    
    [Fact]
    public void Solve_ZeroA_ZeroB_ThrowsException()
    {
        Assert.Throws<InvalidOperationException>(() => 
            LinearEquationSolver.Solve(0, 0));
    }
    
    [Fact]
    public void Solve_ZeroA_NonZeroB_ThrowsException()
    {
        Assert.Throws<InvalidOperationException>(() => 
            LinearEquationSolver.Solve(0, 5));
    }
    
    [Fact]
    public void Solve_NaNInput_ThrowsException()
    {
        Assert.Throws<ArgumentException>(() => 
            LinearEquationSolver.Solve(double.NaN, 5));
    }
    
    [Fact]
    public void TrySolve_Valid_ReturnsTrue()
    {
        var success = LinearEquationSolver.TrySolve(2.5, -5, out var x);
        Assert.True(success);
        Assert.Equal(2.0, x);
    }
    
    [Fact]
    public void TrySolve_NoSolution_ReturnsFalse()
    {
        var success = LinearEquationSolver.TrySolve(0, 5, out var x);
        Assert.False(success);
        Assert.Null(x);
    }
}

public class FractionEquationSolverTests
{
    [Fact]
    public void Solve_ReturnsFraction()
    {
        var x = FractionEquationSolver.Solve(3, 1);
        Assert.Equal(-1, x.Numerator);
        Assert.Equal(3, x.Denominator);
        Assert.Equal("-1/3", x.ToString());
    }
    
    [Fact]
    public void Solve_SimplifiesFraction()
    {
        var x = FractionEquationSolver.Solve(6, 3);
        Assert.Equal(-1, x.Numerator);
        Assert.Equal(2, x.Denominator);
    }
    
    [Fact]
    public void Solve_IntegerResult()
    {
        var x = FractionEquationSolver.Solve(2, -4);
        Assert.Equal(2, x.Numerator);
        Assert.Equal(1, x.Denominator);
        Assert.True(x.IsInteger);
        Assert.Equal("2", x.ToString());
    }
}

public class QuadraticEquationSolverTests
{
    [Fact]
    public void Solve_TwoDistinctSolutions_ReturnsCorrectRoots()
    {
        // x² - 5x + 6 = 0 → x = 2, 3
        var result = QuadraticEquationSolver.Solve(1, -5, 6);
        Assert.True(result.HasSolutions);
        Assert.Equal(2, result.SolutionCount);
        Assert.Equal(2.0, result.X1);
        Assert.Equal(3.0, result.X2);
        Assert.False(result.RepeatedRoot.HasValue);
    }
    
    [Fact]
    public void Solve_OneRepeatedSolution_ReturnsSingleRoot()
    {
        // x² - 4x + 4 = 0 → x = 2
        var result = QuadraticEquationSolver.Solve(1, -4, 4);
        Assert.True(result.HasSolutions);
        Assert.Equal(1, result.SolutionCount);
        Assert.Equal(2.0, result.X1);
        Assert.Null(result.X2);
        Assert.Equal(2.0, result.RepeatedRoot);
    }
    
    [Fact]
    public void Solve_NoRealSolutions_ReturnsNoSolutions()
    {
        // x² + x + 1 = 0
        var result = QuadraticEquationSolver.Solve(1, 1, 1);
        Assert.False(result.HasSolutions);
        Assert.Equal(0, result.SolutionCount);
        Assert.Null(result.RepeatedRoot);
    }
    
    [Fact]
    public void Solve_ZeroA_DelegatesToLinear()
    {
        // 0x² + 2x - 4 = 0 → 2x - 4 = 0 → x = 2
        var result = QuadraticEquationSolver.Solve(0, 2, -4);
        Assert.True(result.HasSolutions);
        Assert.Equal(1, result.SolutionCount);
        Assert.Equal(2.0, result.X1);
    }
    
    [Fact]
    public void Solve_X1LessThanX2()
    {
        // Đảm bảo X1 < X2
        var result = QuadraticEquationSolver.Solve(1, -5, 6);
        Assert.True(result.X1 < result.X2);
    }
    
    [Fact]
    public void TrySolve_InvalidInput_ReturnsNoSolutions()
    {
        var result = QuadraticEquationSolver.TrySolve(double.NaN, 1, 1);
        Assert.False(result.HasSolutions);
    }
}

Tham khảo: Integer Input

public static class LinearEquationSolver_Int
{
    /// <summary>
    /// Giải phương trình bậc nhất (integer version - đơn giản hơn)
    /// </summary>
    public static double Solve(int a, int b)
    {
        if (a != 0)
        {
            return -(double)b / a; // Cast để tránh integer division
        }
        
        if (b == 0)
            throw new InvalidOperationException("Vô số nghiệm");
        
        throw new InvalidOperationException("Vô nghiệm");
    }
}

// Usage
double x = LinearEquationSolver_Int.Solve(2, -4); // x = 2.0

Lưu ý: Integer version đơn giản hơn vì không cần EPSILON, so sánh trực tiếp == 0 được.


Tóm tắt

So sánh Double vs Integer

AspectDouble InputInteger Input
EPSILON✅ Cần❌ Không
ComparisonMath.Abs() > EPSILON!= 0
ValidationNaN, InfinityKhông cần
PrecisionFloating point errorsExact
ComplexityPhức tạp hơnĐơn giản hơn

QuadraticResult Tuple

// Record struct cho type-safe result
public readonly record struct QuadraticResult(
    bool HasSolutions,
    double X1,
    double? X2
);

// Benefits so với array:
✅ Type-safe: Biết rõ ý nghĩa từng field
✅ Self-documenting: HasSolutions, X1, X2 rõ ràng
✅ Pattern matching: var (has, x1, x2) = ...
✅ Extension: Thêm SolutionCount, RepeatedRoot

Design Decisions

Quyết địnhLý do
Static classKhông cần state, tiết kiệm memory
Exception cho errorsGlobal exception handling đã có
TrySolve patternCho expected no solution cases
EPSILON = 1e-10Cân bằng giữa precision và practical
Fraction structExact representation cho integer inputs
Record structType-safe, pattern matching cho quadratic

Performance

Time: O(1) - chỉ vài phép tính
Space: O(1) - không allocate extra memory

← Thuật toán & Giải thuật | Xem bài tiếp theo →

Bài 2: Best Time to Buy and Sell Stock

Đề bài

Cho mảng prices[] chứa giá cổ phiếu theo thứ tự thời gian.

Tính lợi nhuận tối đa với 1 lần mua1 lần bán, phải mua trước khi bán.

Yêu cầu: Chỉ dùng for, không dùng LINQ/third-party.


Phân tích

Input/Output

Input:  [7, 1, 5, 3, 6, 4]
Output: 5
Giải thích: Mua ngày 2 (giá 1), bán ngày 5 (giá 6) → 6 - 1 = 5

Edge cases

  • Mảng rỗng → 0
  • 1 phần tử → 0 (không thể bán)
  • Giá giảm liên tục → 0 (không giao dịch)
  • Null array → Exception

Solution

Final Version - Simple & Optimal

public static class StockProfitSolver
{
    /// <summary>
    /// Tính lợi nhuận tối đa với 1 lần mua-bán
    /// Time: O(n), Space: O(1)
    /// </summary>
    /// <returns>Lợi nhuận tối đa, hoặc 0 nếu không có lợi nhuận</returns>
    public static int CalculateMaxProfit(int[] prices)
    {
        if (prices == null)
            throw new ArgumentNullException(nameof(prices));
        
        if (prices.Length < 2)
            return 0;
        
        int minPrice = int.MaxValue;
        int maxProfit = 0;
        
        for (int i = 0; i < prices.Length; i++)
        {
            // Update minimum price seen so far
            if (prices[i] < minPrice)
            {
                minPrice = prices[i];
            }
            // Calculate profit if selling today
            else if (prices[i] - minPrice > maxProfit)
            {
                maxProfit = prices[i] - minPrice;
            }
        }
        
        return maxProfit;
    }
    
    /// <summary>
    /// Tính lợi nhuận với thông tin chi tiết (buy/sell days)
    /// </summary>
    public static (int profit, int buyDay, int sellDay) CalculateMaxProfitWithDetails(int[] prices)
    {
        if (prices == null)
            throw new ArgumentNullException(nameof(prices));
        
        if (prices.Length < 2)
            return (0, -1, -1);
        
        int minPrice = int.MaxValue;
        int maxProfit = 0;
        int bestBuyDay = -1;
        int bestSellDay = -1;
        int currentBuyDay = 0;
        
        for (int i = 0; i < prices.Length; i++)
        {
            if (prices[i] < minPrice)
            {
                minPrice = prices[i];
                currentBuyDay = i;
            }
            else if (prices[i] - minPrice > maxProfit)
            {
                maxProfit = prices[i] - minPrice;
                bestBuyDay = currentBuyDay;
                bestSellDay = i;
            }
        }
        
        return (maxProfit, bestBuyDay, bestSellDay);
    }
}

Usage

// Basic usage
int[] prices = { 7, 1, 5, 3, 6, 4 };
int profit = StockProfitSolver.CalculateMaxProfit(prices);
// profit = 5

// With details
var (profit, buyDay, sellDay) = StockProfitSolver.CalculateMaxProfitWithDetails(prices);
// profit = 5, buyDay = 1, sellDay = 4
Console.WriteLine($"Mua ngày {buyDay} (giá {prices[buyDay]}), bán ngày {sellDay} (giá {prices[sellDay]})");

// Edge cases
StockProfitSolver.CalculateMaxProfit(new int[] { 7, 6, 4, 3, 1 }); // 0
StockProfitSolver.CalculateMaxProfit(new int[] { 1 }); // 0
StockProfitSolver.CalculateMaxProfit(Array.Empty<int>()); // 0

Unit Tests

using Xunit;

public class StockProfitSolverTests
{
    [Fact]
    public void CalculateMaxProfit_Example1_Returns5()
    {
        int[] prices = { 7, 1, 5, 3, 6, 4 };
        int profit = StockProfitSolver.CalculateMaxProfit(prices);
        Assert.Equal(5, profit);
    }
    
    [Fact]
    public void CalculateMaxProfit_DecreasingPrices_Returns0()
    {
        int[] prices = { 7, 6, 4, 3, 1 };
        int profit = StockProfitSolver.CalculateMaxProfit(prices);
        Assert.Equal(0, profit);
    }
    
    [Fact]
    public void CalculateMaxProfit_IncreasingPrices_ReturnsMaxDifference()
    {
        int[] prices = { 1, 2, 3, 4, 5 };
        int profit = StockProfitSolver.CalculateMaxProfit(prices);
        Assert.Equal(4, profit);
    }
    
    [Fact]
    public void CalculateMaxProfit_NullArray_ThrowsException()
    {
        Assert.Throws<ArgumentNullException>(() => 
            StockProfitSolver.CalculateMaxProfit(null));
    }
    
    [Fact]
    public void CalculateMaxProfit_SingleElement_Returns0()
    {
        int[] prices = { 5 };
        int profit = StockProfitSolver.CalculateMaxProfit(prices);
        Assert.Equal(0, profit);
    }
    
    [Fact]
    public void CalculateMaxProfitWithDetails_ReturnsCorrectDays()
    {
        int[] prices = { 7, 1, 5, 3, 6, 4 };
        var (profit, buyDay, sellDay) = StockProfitSolver.CalculateMaxProfitWithDetails(prices);
        
        Assert.Equal(5, profit);
        Assert.Equal(1, buyDay);
        Assert.Equal(4, sellDay);
        Assert.Equal(1, prices[buyDay]);
        Assert.Equal(6, prices[sellDay]);
    }
    
    [Fact]
    public void CalculateMaxProfitWithDetails_NoProfit_ReturnsNegativeDays()
    {
        int[] prices = { 5, 4, 3, 2, 1 };
        var (profit, buyDay, sellDay) = StockProfitSolver.CalculateMaxProfitWithDetails(prices);
        
        Assert.Equal(0, profit);
        Assert.Equal(-1, buyDay);
        Assert.Equal(-1, sellDay);
    }
}

Mở rộng 1: Multiple Transactions

Đề: Được mua-bán nhiều lần, nhưng phải bán trước khi mua lại.

public static class MultipleTransactionSolver
{
    /// <summary>
    /// Unlimited transactions - Buy at every valley, sell at every peak
    /// Time: O(n), Space: O(1)
    /// </summary>
    public static int CalculateMaxProfit(int[] prices)
    {
        if (prices == null || prices.Length < 2)
            return 0;
        
        int totalProfit = 0;
        
        for (int i = 1; i < prices.Length; i++)
        {
            // If price increases, capture the profit
            if (prices[i] > prices[i - 1])
            {
                totalProfit += prices[i] - prices[i - 1];
            }
        }
        
        return totalProfit;
    }
}

// Usage
int[] prices = { 7, 1, 5, 3, 6, 4 };
// Buy at 1, sell at 5: +4
// Buy at 3, sell at 6: +3
// Total: 7
int profit = MultipleTransactionSolver.CalculateMaxProfit(prices); // 7

// Test
[Fact]
public void MultipleTransactions_Example_Returns7()
{
    int[] prices = { 7, 1, 5, 3, 6, 4 };
    int profit = MultipleTransactionSolver.CalculateMaxProfit(prices);
    Assert.Equal(7, profit);
}

Mở rộng 2: Maximum K Transactions

Đề: Tối đa K lần giao dịch.

public static class KTransactionSolver
{
    /// <summary>
    /// Maximum K transactions - Dynamic Programming
    /// Time: O(kn), Space: O(kn)
    /// </summary>
    public static int CalculateMaxProfit(int[] prices, int k)
    {
        if (prices == null || prices.Length < 2 || k <= 0)
            return 0;
        
        int n = prices.Length;
        
        // If k >= n/2, we can do unlimited transactions
        if (k >= n / 2)
        {
            return MultipleTransactionSolver.CalculateMaxProfit(prices);
        }
        
        // DP: dp[t, d] = max profit with at most t transactions until day d
        int[,] dp = new int[k + 1, n];
        
        for (int t = 1; t <= k; t++)
        {
            int maxDiff = -prices[0];
            
            for (int d = 1; d < n; d++)
            {
                dp[t, d] = Math.Max(dp[t, d - 1], prices[d] + maxDiff);
                maxDiff = Math.Max(maxDiff, dp[t - 1, d] - prices[d]);
            }
        }
        
        return dp[k, n - 1];
    }
}

// Test
[Fact]
public void KTransactions_TwoTransactions_Returns6()
{
    // [3, 3, 5, 0, 0, 3, 1, 4]
    // Buy at 3, sell at 5: +2
    // Buy at 0, sell at 4: +4
    // Total: 6
    int[] prices = { 3, 3, 5, 0, 0, 3, 1, 4 };
    int profit = KTransactionSolver.CalculateMaxProfit(prices, 2);
    Assert.Equal(6, profit);
}

Mở rộng 3: Transaction Fee

Đề: Mỗi lần bán phải trả fee.

public static class StockProfitWithFeeSolver
{
    /// <summary>
    /// Calculate max profit with transaction fee for each sell
    /// Time: O(n), Space: O(1)
    /// </summary>
    public static int CalculateMaxProfit(int[] prices, int fee)
    {
        if (prices == null || prices.Length < 2)
            return 0;
        
        // cash: max profit if we don't hold stock
        // hold: max profit if we hold stock
        int cash = 0;
        int hold = -prices[0];
        
        for (int i = 1; i < prices.Length; i++)
        {
            // Sell or keep not holding
            cash = Math.Max(cash, hold + prices[i] - fee);
            
            // Buy or keep holding
            hold = Math.Max(hold, cash - prices[i]);
        }
        
        return cash;
    }
}

// Test
[Fact]
public void WithFee_Example_Returns4()
{
    // [1, 3, 2, 8, 4, 9], fee = 2
    // Buy at 1, sell at 8: 8 - 1 - 2 = 5
    // Buy at 4, sell at 9: 9 - 4 - 2 = 3
    // But optimal: Buy at 1, sell at 8 = 5
    int[] prices = { 1, 3, 2, 8, 4, 9 };
    int profit = StockProfitWithFeeSolver.CalculateMaxProfit(prices, 2);
    Assert.Equal(8, profit); // Buy at 1, sell at 8: 7, then buy at 4, sell at 9: 3 = 8
}

Tóm tắt

So sánh các versions

VersionTimeSpaceKhi nào dùng
Single transactionO(n)O(1)Bài toán cơ bản
With detailsO(n)O(1)Cần biết buy/sell days
Multiple transactionsO(n)O(1)Unlimited buys/sells
K transactionsO(kn)O(kn)Giới hạn số lần
With feeO(n)O(1)Có transaction cost

Design Decisions

Quyết địnhLý do
Static classKhông cần state, không tốn memory cho object
Return intDirect, dễ dùng
ValueTupleReturn multiple values without class overhead
Exception cho nullGlobal handling đã có
No enum/classSimple is better

Performance

Single transaction:
- Time: O(n) - 1 vòng lặp
- Space: O(1) - chỉ variables

Multiple transactions:
- Time: O(n) - 1 vòng lặp  
- Space: O(1)

K transactions:
- Time: O(k * n) - nested loops
- Space: O(k * n) - DP table

Best Practices (vừa đủ)

✅ Single responsibility
✅ Meaningful names
✅ XML comments
✅ Guard clauses
✅ Unit tests
❌ KHÔNG: Over-engineering


← Bài 1: Linear Equation | Quay lại Thuật toán & Giải thuật

System Design (Thiết kế Hệ thống)

Giới thiệu

System Design là quá trình định nghĩa kiến trúc, components, modules, interfaces và dữ liệu cho một hệ thống để đáp ứng các yêu cầu cụ thể. Trong phỏng vấn kỹ thuật, system design thường được dùng để đánh giá khả năng thiết kế hệ thống phân tán, scalable, reliable và efficient của ứng viên.

Mục tiêu của phần này:

  • Cung cấp các nguyên tắc cơ bản và phương pháp tiếp cận system design.
  • Trình bày các thành phần hệ thống phổ biến và cách kết hợp chúng.
  • Phân tích các case studies tiêu biểu với giải pháp chi tiết.
  • Chuẩn bị cho các câu hỏi phỏng vấn system design.

Mục lục

  1. Nguyên tắc cơ bản - Các nguyên tắc nền tảng của system design

    • Scalability, Reliability, Availability, Maintainability, Performance
  2. Các thành phần hệ thống - Building blocks của hệ thống phân tán

    • Load Balancer, Caching, Database, Message Queue, CDN, API Gateway, Service Discovery
  3. Mô hình kiến trúc - Các kiến trúc phổ biến

    • Monolithic, Microservices, Event-Driven, Serverless, Layered
  4. Phương pháp tiếp cận thiết kế - Quy trình thiết kế hệ thống

    • 6 bước từ thu thập yêu cầu đến đánh giá trade-offs
  5. Các kỹ thuật xử lý - Kỹ thuật giải quyết vấn đề

    • Sharding, Replication, Consistency Models, Rate Limiting, Idempotency, Circuit Breaker
  6. Case Studies - Phân tích các hệ thống thực tế

    • URL Shortener, Chat Application, Social Media Feed, E-commerce, Ride-sharing, Video Streaming
  7. Bài tập & Phỏng vấn - Chuẩn bị cho phỏng vấn

    • Câu hỏi thường gặp, gợi ý trả lời, tài liệu tham khảo

Tổng kết

System design là kỹ năng quan trọng cho các vị trí senior software engineer. Hy vọng phần này cung cấp cho bạn nền tảng vững chắc để tiếp cận các bài toán thiết kế hệ thống trong thực tế và phỏng vấn.

Lưu ý: Các case studies trên chỉ là minh họa; trong thực tế cần điều chỉnh theo yêu cầu cụ thể.

Nguyên tắc cơ bản

Các nguyên tắc nền tảng cần xem xét khi thiết kế hệ thống phân tán.


1. Scalability (Khả năng mở rộng)

Khả năng hệ thống xử lý tăng tải mà không ảnh hưởng đến performance.

  • Vertical Scaling (Scale up): Tăng tài nguyên của một node (CPU, RAM, disk).
  • Horizontal Scaling (Scale out): Thêm nhiều node vào hệ thống.
  • Stateless vs Stateful: Stateless dễ scale hơn vì không cần giữ state giữa các requests.

2. Reliability (Độ tin cậy)

Hệ thống tiếp tục hoạt động đúng ngay cả khi có lỗi (fault tolerance).

  • Redundancy: Nhân bản components để tránh single point of failure.
  • Failover: Tự động chuyển sang backup khi primary fail.
  • Health checks & Monitoring: Phát hiện sự cố sớm.

3. Availability (Tính sẵn sàng)

Tỷ lệ thời gian hệ thống hoạt động và có thể truy cập được.

  • SLA (Service Level Agreement): Thỏa thuận về availability (ví dụ 99.9%).
  • Redundancy & Load balancing: Đảm bảo không có single point of failure.
  • Graceful degradation: Khi một phần hệ thống fail, phần còn lại vẫn hoạt động với chức năng giới hạn.

4. Maintainability (Khả năng bảo trì)

Dễ dàng sửa đổi, cập nhật và vận hành hệ thống.

  • Modularity: Chia hệ thống thành các module độc lập.
  • Documentation & Logging: Ghi lại đầy đủ hoạt động.
  • Automated testing & Deployment: Giảm rủi ro khi thay đổi.

5. Performance (Hiệu suất)

Đo lường bằng throughput, latency, response time.

  • Caching: Giảm latency và tải cho backend.
  • Load balancing: Phân phối tải đều giữa các server.
  • Database optimization: Indexing, sharding, read replicas.

Xem tiếp: Các thành phần hệ thống →

Các thành phần hệ thống

Các building blocks phổ biến để xây dựng hệ thống phân tán.


1. Load Balancer

Phân phối incoming traffic across multiple servers.

  • Algorithms: Round‑Robin, Least Connections, IP Hash, Weighted.
  • Types: Hardware (F5), Software (NGINX, HAProxy), Cloud (AWS ELB, Azure Load Balancer).

2. Caching

Lưu trữ dữ liệu tạm thời để giảm latency và database load.

  • Cache strategies: Cache‑Aside, Read‑Through, Write‑Through, Write‑Behind.
  • Cache invalidation: Time‑based, event‑based.
  • Popular caches: Redis, Memcached, CDN.

3. Database

Lựa chọn database phù hợp với pattern truy cập.

  • SQL (RDBMS): ACID, strong consistency, phù hợp transactional data (MySQL, PostgreSQL).
  • NoSQL: High scalability, flexible schema, phù hợp unstructured data (MongoDB, Cassandra).
  • NewSQL: Kết hợp scalability của NoSQL và ACID của SQL (CockroachDB, Spanner).

4. Message Queue

Decouple components và xử lý bất đồng bộ.

  • Use cases: Background jobs, event‑driven architecture, log aggregation.
  • Examples: Kafka, RabbitMQ, Azure Service Bus, AWS SQS.

5. CDN (Content Delivery Network)

Phân phối nội dung tĩnh đến edge servers gần user.

  • Benefits: Giảm latency, giảm tải origin server.
  • Providers: Cloudflare, Akamai, AWS CloudFront.

6. API Gateway

Entry point cho client, xử lý authentication, rate limiting, routing.

  • Features: SSL termination, request/response transformation, monitoring.
  • Examples: Kong, Apigee, AWS API Gateway.

7. Service Discovery

Tự động detect vị trí của services trong môi trường dynamic.

  • Tools: Consul, etcd, ZooKeeper, Kubernetes Service.

← Nguyên tắc cơ bản | Xem tiếp: Mô hình kiến trúc →

Mô hình kiến trúc

Các kiến trúc hệ thống phổ biến và đặc điểm của chúng.


1. Monolithic Architecture

Toàn bộ ứng dụng được đóng gói thành một unit duy nhất.

  • Ưu điểm: Đơn giản phát triển, testing, deployment.
  • Nhược điểm: Khó scale, công nghệ lock‑in, deployment risk cao.

2. Microservices Architecture

Ứng dụng được chia thành nhiều services độc lập, mỗi service chịu trách nhiệm một business capability.

  • Ưu điểm: Scale độc lập, công nghệ đa dạng, deployment linh hoạt.
  • Nhược điểm: Phức tạp vận hành, network latency, data consistency.

3. Event‑Driven Architecture

Components giao tiếp thông qua events, được publish/subscribe bởi message broker.

  • Ưu điểm: Loose coupling, scalability, real‑time processing.
  • Nhược điểm: Debug khó, eventual consistency.

4. Serverless Architecture

Chạy code mà không cần quản lý server, trả tiền theo thời gian thực thi.

  • Ưu điểm: No server management, auto‑scaling, cost‑effective cho sporadic workload.
  • Nhược điểm: Cold start latency, vendor lock‑in, limited execution time.

5. Layered (N‑Tier) Architecture

Chia ứng dụng thành các layer (presentation, business, data).

  • Ưu điểm: Separation of concerns, dễ bảo trì.
  • Nhược điểm: Performance overhead, khó scale từng layer riêng.

← Các thành phần hệ thống | Xem tiếp: Phương pháp tiếp cận thiết kế →

Phương pháp tiếp cận thiết kế

Quy trình 6 bước để thiết kế một hệ thống trong phỏng vấn hoặc thực tế.


Bước 1: Thu thập yêu cầu (Requirements Gathering)

Xác định rõ ràng những gì hệ thống cần làm.

Functional requirements

  • Chức năng cụ thể hệ thống cần cung cấp.
  • Ví dụ: “User có thể đăng bài”, “User có thể like bài viết”.

Non‑functional requirements

  • Scalability: Số lượng users, requests per second.
  • Availability: SLA yêu cầu (99.9%, 99.99%?).
  • Latency: Response time tối đa cho phép.
  • Consistency: Strong consistency hay eventual consistency?

Ước lượng scale

  • Số lượng users (DAU, MAU).
  • Requests per second (RPS).
  • Data size hiện tại và growth rate.

Bước 2: Ước lượng (Estimation)

Tính toán các con số để định hướng thiết kế.

Traffic estimates

  • RPS (Requests Per Second): RPS = (Total requests per day) / (Seconds per day)
  • Peak RPS: Thường gấp 2-10 lần average RPS.

Storage estimates

  • Dung lượng: Size per item * Items per day * Days
  • Growth rate: Dự kiến tăng trưởng theo tháng/năm.

Bandwidth estimates

  • Upload bandwidth: Data per upload * Uploads per second
  • Download bandwidth: Data per download * Downloads per second

Bước 3: Thiết kế high‑level (High‑Level Design)

Vẽ sơ đồ khối với các components chính.

Components cần xác định

  • Client: Web, Mobile, Desktop.
  • Load Balancer: Phân phối traffic.
  • App Servers: Xử lý business logic.
  • Database: Lưu trữ dữ liệu.
  • Cache: Tăng performance.
  • CDN: Phân phối nội dung tĩnh.
  • Message Queue: Xử lý bất đồng bộ.

Chọn công nghệ

  • Dựa trên requirements và team expertise.
  • Consider trade-offs giữa các lựa chọn.

Bước 4: Thiết kế chi tiết (Detailed Design)

Đi sâu vào từng component và data flow.

Database schema

  • Tables, collections, indexes.
  • Sharding strategy (nếu cần).
  • Partition key selection.

API design

  • Endpoints: GET /api/posts, POST /api/posts.
  • Request/response format (JSON, GraphQL).
  • Authentication & Authorization.

Data flow

  • Sequence diagrams cho các use cases chính.
  • State transitions.

Xử lý edge cases

  • Failure scenarios (database down, network partition).
  • Consistency issues (race conditions, concurrent updates).
  • Retry logic & error handling.

Bước 5: Xác định bottlenecks và tối ưu

Tìm và giải quyết các điểm nghẽn.

Single point of failure

  • Database → Add read replicas, failover.
  • Load balancer → Multiple instances with health checks.
  • Cache → Redis cluster.

Scalability bottlenecks

  • Database → Sharding, read replicas.
  • App server → Horizontal scaling, stateless design.
  • Network → CDN, caching layers.

Performance bottlenecks

  • Slow queries → Indexing, query optimization.
  • Network calls → Batch requests, async processing.
  • Memory → Caching, pagination.

Bước 6: Đánh giá trade‑offs

Mọi thiết kế đều có trade-offs. Cần hiểu và giải thích rõ.

Consistency vs Availability (CAP theorem)

  • CP: Strong consistency, tolerate partitions (banking systems).
  • AP: High availability, eventual consistency (social media).

Latency vs Throughput

  • Low latency: Optimize for fast response time.
  • High throughput: Optimize for number of requests processed.

Cost vs Performance

  • More servers, replicas → Better performance, higher cost.
  • Managed services → Less ops work, higher cost.

Complexity vs Flexibility

  • Microservices → Flexible but complex to operate.
  • Monolith → Simple but hard to scale.

← Mô hình kiến trúc | Xem tiếp: Các kỹ thuật xử lý →

Các kỹ thuật xử lý

Các kỹ thuật và patterns phổ biến để giải quyết vấn đề trong system design.


1. Sharding (Partitioning)

Chia database thành các shard nhỏ hơn dựa trên shard key.

Sharding strategies

  • Range‑based: Chia theo khoảng giá trị (A-M, N-Z).
  • Hash‑based: Dùng hash function để map vào shard.
  • Directory‑based: Lookup table để map key → shard.

Challenges

  • Hotspots: Một shard nhận nhiều traffic hơn.
  • Cross‑shard queries: Query dữ liệu từ nhiều shard phức tạp.
  • Re‑sharding: Khó khăn khi cần thay đổi shard strategy.

2. Replication

Sao chép dữ liệu sang nhiều nodes để tăng availability và reliability.

Replication patterns

  • Master‑Slave: Chỉ master nhận writes, slaves phục vụ reads.
  • Multi‑Master: Nhiều node có thể write, cần conflict resolution.
  • Masterless: Mọi node đều ngang hàng (Cassandra).

Sync vs Async replication

  • Sync: Strong consistency, higher latency.
  • Async: Eventual consistency, lower latency.

3. Consistency Models

Strong consistency

Mọi read nhận được write mới nhất. Phù hợp cho financial transactions.

Eventual consistency

Sau một thời gian, tất cả replicas sẽ converge. Phù hợp cho social media, comments.

CAP theorem

Trong distributed system, chỉ có thể đảm bảo hai trong ba:

  • Consistency: Mọi node thấy cùng một data tại cùng một thời điểm.
  • Availability: Mọi request nhận được response (không guarantee là latest).
  • Partition tolerance: Hệ thống vẫn hoạt động khi có network partition.

4. Rate Limiting

Giới hạn số request từ một client để bảo vệ hệ thống.

Algorithms

  • Token bucket: Tokens được thêm vào bucket với rate cố định.
  • Leaky bucket: Requests được xử lý với rate cố định.
  • Fixed window: Giới hạn số request trong một khoảng thời gian cố định.
  • Sliding window: Giới hạn số request trong khoảng thời gian sliding.

Implementation

  • API Gateway level.
  • Middleware/Service level.
  • Distributed rate limiting với Redis.

5. Idempotency

Đảm bảo thực hiện một operation nhiều lần mà kết quả không thay đổi.

Techniques

  • Unique request ID: Client gửi ID, server check nếu đã xử lý thì return kết quả cũ.
  • Idempotent API design: PUT /users/123 luôn update cùng một trạng thái.
  • Database constraints: Unique key để tránh duplicate.

Use cases

  • Payment processing (tránh double charge).
  • Order creation.
  • Form submissions.

6. Circuit Breaker

Ngăn hệ thống gọi service đang fail liên tục.

States

  • Closed: Bình thường, calls được thực hiện.
  • Open: Service đang fail, calls bị chặn ngay lập tức.
  • Half‑Open: Thử một vài calls để check service đã recovery chưa.

Implementation

  • C#: Polly library.
  • Java: Hystrix, Resilience4j.
  • Go: gobreaker.

← Phương pháp tiếp cận thiết kế | Xem tiếp: Case Studies →

Case Studies

Các case studies được phân tích theo quy trình 6 bước đã trình bày ở Phương pháp tiếp cận thiết kế.


Danh sách Case Studies

  1. URL Shortener (TinyURL) - Hệ thống rút gọn URL
  2. Chat Application (WhatsApp/Telegram) - Ứng dụng chat real-time
  3. Social Media Feed (Twitter) - News feed cho mạng xã hội
  4. E‑commerce Platform (Amazon) - Sàn thương mại điện tử
  5. Ride‑sharing (Uber) - Ứng dụng gọi xe
  6. Video Streaming (YouTube/Netflix) - Nền tảng streaming video

Cấu trúc mỗi Case Study

Mỗi case study được phân tích theo các bước:

  1. Bước 1: Thu thập yêu cầu - Functional & Non-functional requirements, Scale estimation
  2. Bước 2: Ước lượng - Traffic, Storage, Bandwidth estimates
  3. Bước 3: Thiết kế high‑level - Block diagram, Components, Technology selection
  4. Bước 4: Thiết kế chi tiết - Database schema, API design, Data flow
  5. Bước 5: Bottlenecks & Tối ưu - SPOF, Scalability, Performance
  6. Bước 6: Trade‑offs - Consistency vs Availability, Latency vs Throughput, Cost vs Performance

← Các kỹ thuật xử lý | Xem tiếp: URL Shortener →

Case Study 1: URL Shortener (TinyURL)

Hệ thống rút gọn URL, tương tự TinyURL, bit.ly.


Bước 1: Thu thập yêu cầu

Functional requirements

  • Chuyển đổi URL dài thành URL ngắn.
  • Redirect người dùng khi truy cập URL ngắn.
  • Custom alias (optional).
  • Analytics: số lần click, location, referrer (optional).

Non‑functional requirements

  • High availability: URL luôn accessible.
  • Low latency: Redirect < 100ms.
  • Scalability: Hàng tỷ URLs.
  • Durability: Dữ liệu không bị mất.

Scale estimation

  • Users: 500 triệu URLs mới mỗi tháng.
  • Redirects: 10 tỷ redirects mỗi tháng.
  • Read/Write ratio: ~20:1 (nhiều reads hơn writes).

Bước 2: Ước lượng

Traffic estimates

  • Create URL: 500M / (30 days * 24h * 3600s) ≈ 200 RPS (average).
  • Redirect: 10B / (30 days * 24h * 3600s) ≈ 4,000 RPS (average).
  • Peak RPS: ~10x average → 2,000 create RPS, 40,000 redirect RPS.

Storage estimates

  • Data per URL: short_key (7 bytes) + original_url (500 bytes) + metadata (100 bytes) ≈ 600 bytes.
  • Monthly storage: 500M * 600 bytes ≈ 300 GB.
  • 5 years: 300 GB * 60 ≈ 18 TB.

Bandwidth estimates

  • Upload: 200 RPS * 500 bytes ≈ 100 KB/s.
  • Download: 4,000 RPS * 500 bytes ≈ 2 MB/s.

Bước 3: Thiết kế High‑Level

Components chính

┌──────────┐     ┌─────────────┐     ┌──────────────┐     ┌──────────┐
│  Client  │ ──→ │ Load        │ ──→ │ App Servers  │ ──→ │ Database │
│          │     │ Balancer    │     │ (Stateless)  │     │          │
└──────────┘     └─────────────┘     └──────────────┘     └──────────┘
                      │                    │
                      │                    ▼
                      │              ┌──────────┐
                      └──────────────│  Cache   │
                                     │ (Redis)  │
                                     └──────────┘

Technology selection

  • Load Balancer: NGINX hoặc AWS ELB.
  • App Servers: Stateless, horizontal scaling.
  • Database: NoSQL (Cassandra/DynamoDB) cho scale, hoặc SQL (PostgreSQL) với sharding.
  • Cache: Redis cho frequently accessed URLs.
  • Storage: 18 TB sau 5 năm → Distributed storage.

Bước 4: Thiết kế Chi tiết

Database Schema

Table: url_mappings

ColumnTypeDescription
short_keyVARCHAR(7)Primary key, unique
original_urlTEXTURL gốc
user_idBIGINTOwner (optional)
created_atTIMESTAMPThời gian tạo
click_countBIGINTSố lần click

Encoding Strategy

Base62 Encoding (a-z, A-Z, 0-9):

  • 62 characters, 7 characters → 62^7 ≈ 3.5 trillion combinations.
  • Counter-based: Dùng auto-increment ID từ database, encode sang base62.
  • Hash-based: MD5/SHA256 → take first 7 chars (collision handling needed).

API Design

POST /api/v1/shorten
{
  "url": "https://example.com/very/long/url"
}

Response:
{
  "short_key": "abc123",
  "short_url": "https://tiny.url/abc123"
}

GET /abc123
→ 301 Redirect to original URL

Data Flow

Create URL:

  1. Client gửi URL dài → API.
  2. Generate unique ID (counter/hash).
  3. Encode ID → short_key.
  4. Lưu vào database + cache.
  5. Return short_url.

Redirect:

  1. Client request /abc123.
  2. Check cache → nếu có, redirect ngay.
  3. Nếu không có, query database.
  4. Lưu vào cache.
  5. 301 Redirect.
  6. Increment click_count (async).

Bước 5: Bottlenecks & Tối ưu

Single Point of Failure

  • Database: Dùng replication (1 master, nhiều slaves).
  • Cache: Redis cluster với sentinel.
  • Load Balancer: Multiple instances với health checks.

Scalability Bottlenecks

  • Database write: Sharding theo short_key hoặc user_id.
  • Database read: Read replicas, cache hit rate > 90%.
  • Counter service: Distributed ID generation (Twitter Snowflake).

Performance Optimization

  • Cache strategy: Cache-aside cho redirects.
  • CDN: Cache redirect responses cho static URLs.
  • Async analytics: Dùng message queue để track clicks.

Bước 6: Trade‑offs

Consistency vs Availability

  • AP system: Eventual consistency acceptable cho redirects.
  • Cache có thể stale trong vài giây → user có thể thấy URL cũ.

Latency vs Throughput

  • Low latency: Cache-first design, 301 redirect.
  • High throughput: Async analytics processing.

Cost vs Performance

  • Managed database (DynamoDB): Đắt hơn nhưng auto-scaling.
  • Self-hosted (Cassandra): Rẻ hơn nhưng cần ops team.

Encoding: Counter vs Hash

ApproachProsCons
CounterUnique, no collisionPredictable, need distributed counter
HashNon-predictableCollision handling, longer storage

Kết luận

Hệ thống URL shortener là bài toán cơ bản nhưng cover nhiều khía cạnh quan trọng:

  • Encoding strategies cho short URLs.
  • Cache design cho read-heavy workload.
  • Database sharding cho scale.
  • Async processing cho analytics.

← Case Studies Index | Xem tiếp: Chat Application →

Case Study 2: Chat Application (WhatsApp/Telegram)

Ứng dụng chat real-time với support group chat, multimedia, presence.


Bước 1: Thu thập yêu cầu

Functional requirements

  • 1-on-1 messaging: Gửi/nhận tin nhắn giữa 2 users.
  • Group chat: Support đến 1000 participants.
  • Multimedia: Images, videos, voice messages.
  • Presence: Online/offline status, last seen.
  • Read receipts: Delivered, read status.
  • Push notifications: Cho offline users.

Non‑functional requirements

  • Low latency: Message delivery < 500ms.
  • High availability: 99.9% uptime.
  • Scalability: Hàng tỷ users, triệu concurrent connections.
  • Reliability: Không mất tin nhắn.
  • Ordering: Messages hiển thị đúng thứ tự.

Scale estimation

  • Users: 1 tỷ DAU (Daily Active Users).
  • Concurrent connections: 100 triệu.
  • Messages per day: 100 tỷ.
  • Media uploads: 10 tỷ mỗi ngày.

Bước 2: Ước lượng

Traffic estimates

  • Messages: 100B / 86400 ≈ 1.15 triệu RPS (average).
  • Peak RPS: ~5x average → 6 triệu RPS.
  • Concurrent connections: 100 triệu WebSocket connections.

Storage estimates

  • Text message: 1 KB per message.
  • Daily text storage: 100B * 1 KB ≈ 100 TB.
  • Media storage: 10B * 500 KB ≈ 5 PB mỗi ngày.
  • 5 years: ~10 EB (chưa tính replication).

Bandwidth estimates

  • Upload: 1.15M RPS * 1 KB ≈ 1.15 GB/s (text only).
  • Download: Fan-out 10x → 11.5 GB/s.
  • Media: Significant higher, cần CDN.

Bước 3: Thiết kế High‑Level

Components chính

┌──────────┐     ┌─────────────┐     ┌──────────────┐
│  Client  │ ──→ │ API Gateway │ ──→ │ Chat Service │
│ (WebSocket)│    │ (SSL term)  │     │ (Stateless)  │
└──────────┘     └─────────────┘     └──────────────┘
                                            │
              ┌─────────────────────────────┼─────────────────────────────┐
              ▼                             ▼                             ▼
       ┌──────────────┐            ┌──────────────┐            ┌──────────────┐
       │ Message Queue│            │   Database   │            │    Cache     │
       │   (Kafka)    │            │ (Cassandra)  │            │   (Redis)    │
       └──────────────┘            └──────────────┘            └──────────────┘
              │
              ▼
       ┌──────────────┐
       │   Delivery   │
       │   Service    │
       └──────────────┘

Technology selection

  • Connection protocol: WebSocket (primary), Long polling (fallback).
  • Load Balancer: NGINX với WebSocket support.
  • App Servers: Stateless, horizontal scaling.
  • Message Queue: Kafka cho durability và replayability.
  • Database: Cassandra cho write-heavy, time-series data.
  • Cache: Redis cho presence, session, recent messages.
  • Media storage: S3 + CDN (CloudFront).

Bước 4: Thiết kế Chi tiết

Database Schema

Table: messages

ColumnTypeDescription
message_idUUIDPrimary key
chat_idBIGINTChat room ID (indexed)
sender_idBIGINTUser ID
contentTEXTMessage content
media_urlTEXTOptional media link
created_atTIMESTAMPTime (clustering key)
statusTINYINTSent/Delivered/Read

Table: chat_members

ColumnTypeDescription
chat_idBIGINTChat ID
user_idBIGINTUser ID
roleTINYINTAdmin/Member
joined_atTIMESTAMPJoin time

Table: user_presence

ColumnTypeDescription
user_idBIGINTPrimary key (Redis)
statusTINYINTOnline/Offline
last_seenTIMESTAMPLast activity

API Design

WebSocket Connection:
wss://chat.example.com/ws?token=xxx

Message Send:
{
  "type": "message",
  "chat_id": 12345,
  "content": "Hello!",
  "media_url": null
}

Message Receive:
{
  "type": "message",
  "message_id": "uuid",
  "chat_id": 12345,
  "sender_id": 67890,
  "content": "Hello!",
  "created_at": "2024-01-01T12:00:00Z"
}

Presence Update:
{
  "type": "presence",
  "user_id": 67890,
  "status": "online"
}

Data Flow

Send Message (1-on-1):

  1. Client gửi message qua WebSocket.
  2. Chat Service nhận, validate.
  3. Lưu message vào database (partition by chat_id).
  4. Publish event đến Kafka topic chat-{chat_id}.
  5. Delivery Service subscribe, forward đến recipient.
  6. Update cache (recent messages).
  7. Send push notification nếu recipient offline.

Receive Message:

  1. Delivery Service nhận từ Kafka.
  2. Lookup recipient’s connection (which server).
  3. Forward qua WebSocket connection.
  4. Client ack → update status thành “delivered”.
  5. Khi user đọc → update “read” status.

Presence System:

  1. Khi client connect → set Redis key presence:{user_id} = online.
  2. Heartbeat mỗi 30s để giữ connection.
  3. Khi disconnect/timer expire → set offline + last_seen.
  4. Subscribe presence của contacts để nhận updates.

Bước 5: Bottlenecks & Tối ưu

Single Point of Failure

  • WebSocket connections: Multiple servers với sticky sessions.
  • Database: Cassandra replication factor 3.
  • Kafka: Multiple brokers, replicated topics.
  • Redis: Sentinel hoặc cluster mode.

Scalability Bottlenecks

  • Connection scaling: Mỗi server handle ~100k connections → cần 1000 servers cho 100M concurrent.
  • Database write: Cassandra auto-sharding theo partition key.
  • Message fan-out: Group chat với 1000 members → batch delivery.

Performance Optimization

  • Message ordering: Dùng timestamp + sequence number.
  • Recent messages cache: Redis sorted set cho last 50 messages.
  • Media optimization: Compress images, adaptive bitrate cho videos.
  • Batch updates: Read receipts batched mỗi 5s.

Bước 6: Trade‑offs

Consistency vs Availability

  • AP system: Eventual consistency cho messages và presence.
  • Message có thể delay vài giây nhưng không mất.
  • Presence có thể stale trong 30-60s.

Latency vs Throughput

  • Low latency: WebSocket persistent connection.
  • High throughput: Batch message delivery cho group chat.

Cost vs Performance

  • Managed services (AWS): Đắt hơn nhưng auto-scaling.
  • Self-hosted: Rẻ hơn nhưng cần large ops team 24/7.

WebSocket vs Long Polling

ApproachProsCons
WebSocketReal-time, low latency, bidirectionalComplex, battery drain
Long PollingSimple, works everywhereHigher latency, more connections

Kết luận

Chat application là hệ thống phức tạp với nhiều challenges:

  • Connection management cho triệu concurrent users.
  • Message ordering & delivery guarantee.
  • Presence system với low latency.
  • Media handling với storage và bandwidth lớn.
  • Push notifications cho offline users.

← URL Shortener | Xem tiếp: Social Media Feed →

Case Study 3: Social Media Feed (Twitter)

News feed cho mạng xã hội với posts, likes, retweets, follows.


Bước 1: Thu thập yêu cầu

Functional requirements

  • Post tweets: Text (280 chars), images, videos.
  • Follow/Unfollow: User có thể follow others.
  • News feed: Hiển thị tweets từ people user follow.
  • Like/Retweet: Interactions với tweets.
  • Search: Tìm tweets, users.
  • Trending: Hashtags trending.

Non‑functional requirements

  • Low latency: Feed load < 200ms.
  • High availability: 99.9% uptime.
  • Scalability: Hàng tỷ users, triệu tweets/ngày.
  • Consistency: Feed nên relatively fresh.

Scale estimation

  • Users: 500 triệu DAU.
  • Tweets per day: 500 triệu.
  • Follows per user: Average 200.
  • Read/Write ratio: ~100:1 (nhiều reads hơn writes).
  • Celebrities: Vài users có > 100M followers.

Bước 2: Ước lượng

Traffic estimates

  • Tweet writes: 500M / 86400 ≈ 6,000 RPS (average).
  • Feed reads: 500M DAU * 10 feeds/day / 86400 ≈ 60,000 RPS.
  • Peak RPS: ~5x average → 30,000 write, 300,000 read RPS.

Storage estimates

  • Tweet size: 1 KB (text + metadata).
  • Daily storage: 500M * 1 KB ≈ 500 GB.
  • Media storage: 500M * 100 KB ≈ 50 TB mỗi ngày.
  • 5 years: ~100 TB (text), 100 PB (media).

Bandwidth estimates

  • Upload: 6,000 RPS * 1 KB ≈ 6 MB/s (text).
  • Download: 60,000 RPS * 100 tweets * 1 KB ≈ 6 GB/s.

Bước 3: Thiết kế High‑Level

Components chính

┌──────────┐     ┌─────────────┐     ┌──────────────┐
│  Client  │ ──→ │ Load        │ ──→ │ Tweet        │
│          │     │ Balancer    │     │ Service      │
└──────────┘     └─────────────┘     └──────────────┘
                                            │
              ┌─────────────────────────────┼─────────────────────────────┐
              ▼                             ▼                             ▼
       ┌──────────────┐            ┌──────────────┐            ┌──────────────┐
       │    Search    │            │   Database   │            │    Cache     │
       │  (Elastic)   │            │ (Cassandra)  │            │   (Redis)    │
       └──────────────┘            └──────────────┘            └──────────────┘
                                            │
                                            ▼
                                   ┌──────────────┐
                                   │  Feed        │
                                   │  Generator   │
                                   └──────────────┘

Technology selection

  • Load Balancer: AWS ELB hoặc NGINX.
  • App Services: Microservices (Tweet, User, Feed, Search).
  • Database: Cassandra cho tweets, MySQL cho user data.
  • Cache: Redis cho feeds, trending, counts.
  • Search: Elasticsearch cho full-text search.
  • Media: S3 + CDN.

Bước 4: Thiết kế Chi tiết

Database Schema

Table: tweets

ColumnTypeDescription
tweet_idBIGINTPrimary key (Snowflake ID)
user_idBIGINTAuthor ID (indexed)
contentTEXTTweet text
media_urlsJSONOptional media links
created_atTIMESTAMPTime (clustering key)
like_countINTDenormalized count
retweet_countINTDenormalized count

Table: follows

ColumnTypeDescription
follower_idBIGINTUser who follows
followee_idBIGINTUser being followed
created_atTIMESTAMPFollow time
Primary Key: (follower_id, followee_id)

Table: user_feed (Redis)

KeyTypeDescription
feed:{user_id}Sorted SetTweet IDs với score = timestamp

Feed Generation Strategies

1. Pull Model (Lazy)

  • Khi user request feed, query tweets từ tất cả followees.
  • Pros: Đơn giản, real-time.
  • Cons: Chậm với users follow nhiều people.
SELECT t.* FROM tweets t
JOIN follows f ON t.user_id = f.followee_id
WHERE f.follower_id = :user_id
ORDER BY t.created_at DESC
LIMIT 100

2. Push Model (Pre-computed)

  • Khi user post tweet, push vào feed cache của tất cả followers.
  • Pros: Feed load rất nhanh.
  • Cons: Tốn storage, write amplification cho celebrities.
On tweet:
for each follower in followers[user_id]:
    redis.zadd("feed:" + follower_id, tweet_id, timestamp)
  • Normal users: Push model.
  • Celebrities (> 1M followers): Pull model.
  • Feed = Merge(precomputed_feed, celebrity_tweets).

API Design

POST /api/v1/tweets
{
  "content": "Hello, world!",
  "media_urls": ["https://..."]
}

GET /api/v1/feed?cursor=xxx&limit=20

Response:
{
  "tweets": [
    {
      "tweet_id": 123456,
      "user": { "id": 1, "name": "User" },
      "content": "Hello!",
      "created_at": "2024-01-01T12:00:00Z",
      "like_count": 100,
      "retweet_count": 50
    }
  ],
  "next_cursor": "yyy"
}

POST /api/v1/tweets/{id}/like
POST /api/v1/tweets/{id}/retweet

Data Flow

Post Tweet:

  1. Client POST /tweets.
  2. Validate, store in Cassandra.
  3. Lookup followers (from cache).
  4. If normal user: Push to followers’ feed cache.
  5. If celebrity: Store only, pull on read.
  6. Update search index (async via Kafka).
  7. Return tweet.

Load Feed:

  1. Client GET /feed.
  2. Load precomputed feed from Redis (50 tweets).
  3. Fetch recent tweets from celebrities (pull).
  4. Merge & sort by timestamp.
  5. Return feed.
  6. Cache result.

Bước 5: Bottlenecks & Tối ưu

Single Point of Failure

  • Database: Cassandra replication factor 3.
  • Cache: Redis cluster với sentinel.
  • Feed Generator: Multiple instances.

Scalability Bottlenecks

  • Celebrity fan-out: Justin Bieber (100M followers) → 100M writes!
    • Solution: Hybrid model, celebrity tweets pulled on read.
  • Feed cache memory: 500M users * 50 tweets * 8 bytes ≈ 200 GB.
    • Solution: Only cache active users, LRU eviction.

Performance Optimization

  • Pagination: Cursor-based pagination (dùng timestamp).
  • Denormalization: Store like_count, retweet_count trong tweet.
  • Async updates: Like/retweet counts update async qua Kafka.
  • CDN: Cache media files, static content.

Bước 6: Trade‑offs

Consistency vs Availability

  • AP system: Eventual consistency cho feed và counts.
  • Like count có thể delay vài giây.
  • Feed có thể thiếu tweets mới trong vài giây.

Latency vs Throughput

  • Low latency: Precomputed feed (push model).
  • High throughput: Batch feed updates mỗi 5-10s.

Push vs Pull Trade-off

ModelWrite CostRead CostBest For
PullO(1)O(N * M)Celebrities
PushO(N)O(1)Normal users
HybridO(min(N, M))O(log N)Mixed

N = followers, M = followees

Cost vs Performance

  • Managed Cassandra (DataStax): Đắt nhưng auto-scaling.
  • Self-hosted: Rẻ hơn nhưng cần ops expertise.

Kết luận

Social media feed là bài toán kinh điển về read-heavy system với challenges:

  • Feed generation strategy: Push vs Pull vs Hybrid.
  • Celebrity problem: Fan-out amplification.
  • Real-time requirements: Feed phải fresh.
  • Scale: Hàng tỷ users, triệu tweets/ngày.

← Chat Application | Xem tiếp: E‑commerce Platform →

Case Study 4: E‑commerce Platform (Amazon)

Sàn thương mại điện tử với product catalog, cart, order, payment, recommendations.


Bước 1: Thu thập yêu cầu

Functional requirements

  • Product catalog: Browse, search, filter products.
  • Shopping cart: Add/remove items, update quantity.
  • Order processing: Checkout, payment, shipping.
  • Inventory management: Track stock levels.
  • Recommendations: Personalized product suggestions.
  • User accounts: Profiles, order history, addresses.
  • Reviews & Ratings: User feedback.

Non‑functional requirements

  • High availability: 99.99% uptime (critical for sales).
  • Consistency: Inventory và orders phải consistent.
  • Low latency: Page load < 2s, search < 200ms.
  • Scalability: Handle traffic spikes (Black Friday, sales).
  • Security: PCI DSS compliance cho payment.

Scale estimation

  • Users: 100 triệu DAU.
  • Products: 100 triệu SKUs.
  • Orders per day: 10 triệu.
  • Page views per day: 1 tỷ.
  • Peak traffic: 10x normal (sales events).

Bước 2: Ước lượng

Traffic estimates

  • Page views: 1B / 86400 ≈ 12,000 RPS (average).
  • Search requests: 100M / 86400 ≈ 1,200 RPS.
  • Orders: 10M / 86400 ≈ 120 RPS.
  • Peak RPS: ~10x → 120,000 page views, 1,200 orders.

Storage estimates

  • Product catalog: 100M products * 10 KB ≈ 1 TB.
  • User data: 100M users * 1 KB ≈ 100 GB.
  • Orders: 10M/day * 1 KB * 365 * 5 ≈ 18 TB (5 years).
  • Images: 100M products * 5 images * 500 KB ≈ 250 TB.

Bandwidth estimates

  • Page load: 12,000 RPS * 100 KB ≈ 1.2 GB/s.
  • Images: Significant, cần CDN.

Bước 3: Thiết kế High‑Level

Components chính

┌──────────┐     ┌─────────────┐     ┌──────────────┐
│  Client  │ ──→ │ CDN / WAF   │ ──→ │ API Gateway  │
│          │     │ (CloudFlare)│     │              │
└──────────┘     └─────────────┘     └──────────────┘
                                            │
        ┌───────────────────────────────────┼───────────────────────────────────┐
        ▼                                   ▼                                   ▼
┌───────────────┐                  ┌───────────────┐                  ┌───────────────┐
│   Product     │                  │    Cart       │                  │    Order      │
│   Service     │                  │    Service    │                  │    Service    │
└───────────────┘                  └───────────────┘                  └───────────────┘
        │                                   │                                   │
        ▼                                   ▼                                   ▼
┌───────────────┐                  ┌───────────────┐                  ┌───────────────┐
│  PostgreSQL   │                  │    Redis      │                  │  PostgreSQL   │
│  (Products)   │                  │    (Cart)     │                  │  (Orders)     │
└───────────────┘                  └───────────────┘                  └───────────────┘
        │
        ▼
┌───────────────┐
│ Elasticsearch │
│   (Search)    │
└───────────────┘

Technology selection

  • Frontend: React/Next.js với SSR.
  • API Gateway: Kong hoặc AWS API Gateway.
  • Services: Microservices (Java/Spring hoặc Node.js).
  • Database: PostgreSQL cho transactions, Redis cho cart.
  • Search: Elasticsearch cho product search.
  • Cache: Redis cho sessions, cart, popular products.
  • CDN: CloudFront cho static content, images.
  • Message Queue: Kafka cho events (order placed, inventory updated).

Bước 4: Thiết kế Chi tiết

Database Schema

Table: products

ColumnTypeDescription
product_idBIGINTPrimary key
titleVARCHAR(500)Product name
descriptionTEXTProduct description
priceDECIMAL(10,2)Current price
category_idBIGINTForeign key
inventory_countINTStock level
created_atTIMESTAMPCreated time

Table: users

ColumnTypeDescription
user_idBIGINTPrimary key
emailVARCHAR(255)Unique, indexed
password_hashVARCHAR(255)Hashed password
created_atTIMESTAMPJoin date

Table: orders

ColumnTypeDescription
order_idUUIDPrimary key
user_idBIGINTForeign key
total_amountDECIMAL(10,2)Order total
statusTINYINTPending/Paid/Shipped/Delivered
created_atTIMESTAMPOrder time
Index: (user_id, created_at)

Table: order_items

ColumnTypeDescription
order_idUUIDForeign key
product_idBIGINTForeign key
quantityINTQuantity ordered
priceDECIMAL(10,2)Price at time of order

API Design

GET /api/v1/products?category=electronics&q=laptop&page=1&limit=20

GET /api/v1/products/{id}

POST /api/v1/cart/items
{
  "product_id": 12345,
  "quantity": 2
}

GET /api/v1/cart

POST /api/v1/orders
{
  "items": [
    { "product_id": 12345, "quantity": 2 }
  ],
  "shipping_address": {...},
  "payment_method": {...}
}

POST /api/v1/payments
{
  "order_id": "uuid",
  "card_token": "tok_xxx"
}

Data Flow

Browse Products:

  1. Client request /products với filters.
  2. API Gateway → Product Service.
  3. Query Elasticsearch với filters.
  4. Return results với pagination.
  5. Cache popular searches.

Add to Cart:

  1. Client POST /cart/items.
  2. Cart Service validate product availability.
  3. Store in Redis hash: cart:{user_id}.
  4. Set TTL 30 days.
  5. Return updated cart.

Checkout:

  1. Client POST /orders.
  2. Order Service create order (PENDING).
  3. Reserve inventory (decrement count).
  4. Call Payment Service.
  5. Payment success → update order status (PAID).
  6. Publish order.placed event.
  7. Inventory Service consume event, update stock.
  8. Shipping Service consume event, prepare shipment.

Payment Processing:

  1. Payment Service call Stripe/PayPal API.
  2. Handle 3D Secure nếu cần.
  3. Store payment result.
  4. Idempotency key để tránh double charge.
  5. Webhook handling cho async payment status.

Bước 5: Bottlenecks & Tối ưu

Single Point of Failure

  • Database: PostgreSQL với streaming replication (1 master, 2 slaves).
  • Payment: Multiple payment providers (Stripe + PayPal).
  • API Gateway: Multiple instances với health checks.

Scalability Bottlenecks

  • Database write: Orders table partitioning theo created_at (monthly).
  • Search: Elasticsearch cluster với sharding.
  • Cart: Redis cluster cho horizontal scaling.
  • Inventory: Optimistic locking để tránh overselling.

Performance Optimization

  • CDN: Cache product images, static assets.
  • Database read replicas: Cho product browsing.
  • Caching:
    • Redis cho popular products, categories.
    • Application-level cache cho product details.
  • Async processing:
    • Email notifications via Kafka.
    • Recommendation updates batched.

Inventory Consistency

  • Optimistic locking: UPDATE products SET inventory = inventory - 1 WHERE id = ? AND inventory > 0.
  • Reserve timeout: Release reserved inventory sau 15 phút nếu không thanh toán.
  • Queue-based: Serialze inventory updates qua message queue.

Bước 6: Trade‑offs

Consistency vs Availability

  • CP system cho inventory và orders: Strong consistency required.
  • AP system cho product browsing: Eventual consistency acceptable.

Latency vs Throughput

  • Checkout: Low latency critical → synchronous processing.
  • Notifications: High throughput → async via Kafka.

Monolith vs Microservices

ApproachProsCons
MonolithSimple, ACID transactionsHard to scale, deployment risk
MicroservicesIndependent scaling, deploymentDistributed transactions, complexity

Database per Service vs Shared Database

  • Database per service: Isolation, independent scaling, nhưng cần saga pattern cho transactions.
  • Shared database: Easier transactions, nhưng coupling và scaling khó.

Kết luận

E-commerce platform là hệ thống phức tạp với nhiều requirements:

  • Transaction integrity: Payment và inventory phải consistent.
  • High availability: Downtime = mất revenue.
  • Traffic spikes: Black Friday, flash sales.
  • Security: PCI DSS, user data protection.
  • Personalization: Recommendations, search ranking.

← Social Media Feed | Xem tiếp: Ride‑sharing →

Case Study 5: Ride‑sharing (Uber/Grab)

Ứng dụng gọi xe với real-time matching, tracking, pricing, payment.


Bước 1: Thu thập yêu cầu

Functional requirements

  • Ride request: Rider request ride với pickup/dropoff locations.
  • Driver matching: Find nearby available drivers.
  • Real-time tracking: Track driver location và ETA.
  • Pricing: Fare estimation, surge pricing.
  • Payment: Cashless payment, tipping.
  • Rating: Driver/rider ratings.
  • Ride history: Past trips, receipts.

Non‑functional requirements

  • Low latency: Match < 5s, real-time tracking < 1s update.
  • High availability: 99.9% uptime.
  • Scalability: Triệu concurrent rides, triệu drivers.
  • Consistency: Pricing và payment phải consistent.
  • Location accuracy: < 10m error.

Scale estimation

  • Cities: 100 cities worldwide.
  • Drivers: 5 triệu active drivers.
  • Riders: 50 triệu active riders.
  • Rides per day: 20 triệu.
  • Location updates: 50 triệu/minute (drivers sending GPS).

Bước 2: Ước lượng

Traffic estimates

  • Ride requests: 20M / 86400 ≈ 230 RPS (average).
  • Location updates: 50M / 60s ≈ 830,000 RPS.
  • Match requests: ~230 RPS (one per ride request).
  • Peak RPS: ~5x average → 4 triệu location updates/s.

Storage estimates

  • Ride data: 20M/day * 1 KB ≈ 20 GB/day36 TB (5 years).
  • Location history: 50M/min * 100 bytes * 60 * 24 ≈ 7 TB/day.
  • User data: 55M users * 1 KB ≈ 55 GB.
  • Total 5 years: ~15 PB (chủ yếu location history).

Bandwidth estimates

  • Location upload: 830k RPS * 100 bytes ≈ 83 MB/s.
  • Tracking download: Fan-out to riders ≈ 500 MB/s.

Bước 3: Thiết kế High‑Level

Components chính

┌──────────┐     ┌─────────────┐     ┌──────────────┐
│  Client  │ ──→ │ Load        │ ──→ │ API Gateway  │
│ (Mobile) │     │ Balancer    │     │              │
└──────────┘     └─────────────┘     └──────────────┘
                                            │
        ┌───────────────────────────────────┼───────────────────────────────────┐
        ▼                                   ▼                                   ▼
┌───────────────┐                  ┌───────────────┐                  ┌───────────────┐
│   Ride        │                  │   Location    │                  │    Dispatch   │
│   Service     │                  │   Service     │                  │    Service    │
└───────────────┘                  └───────────────┘                  └───────────────┘
        │                                   │                                   │
        ▼                                   ▼                                   ▼
┌───────────────┐                  ┌───────────────┐                  ┌───────────────┐
│  PostgreSQL   │                  │   Redis +     │                  │    Kafka      │
│   (Rides)     │                  │  GeoIndex     │                  │   (Events)    │
└───────────────┘                  └───────────────┘                  └───────────────┘
        │
        ▼
┌───────────────┐
│   Payment     │
│   Service     │
└───────────────┘

Technology selection

  • Mobile: iOS (Swift), Android (Kotlin) với real-time updates.
  • API Gateway: Kong với WebSocket support.
  • Location Service: Redis GeoHash hoặc Elasticsearch geo queries.
  • Database: PostgreSQL cho rides, users; Cassandra cho location history.
  • Cache: Redis cho driver locations, availability.
  • Message Queue: Kafka cho location streams, ride events.
  • Real-time: WebSocket cho driver tracking.

Bước 4: Thiết kế Chi tiết

Database Schema

Table: rides

ColumnTypeDescription
ride_idUUIDPrimary key
rider_idBIGINTForeign key
driver_idBIGINTForeign key (nullable khi chưa match)
pickup_latDECIMAL(9,6)Pickup location
pickup_lngDECIMAL(9,6)Pickup location
dropoff_latDECIMAL(9,6)Dropoff location
dropoff_lngDECIMAL(9,6)Dropoff location
statusTINYINTRequested/Matched/InProgress/Completed/Cancelled
fareDECIMAL(10,2)Final fare
created_atTIMESTAMPRequest time

Table: drivers

ColumnTypeDescription
driver_idBIGINTPrimary key
user_idBIGINTForeign key
vehicle_infoJSONCar model, plate, color
ratingDECIMAL(3,2)Average rating
statusTINYINTAvailable/Busy/Offline

Table: locations (Time-series, partitioned)

ColumnTypeDescription
driver_idBIGINTPartition key
timestampTIMESTAMPClustering key
latDECIMAL(9,6)GPS latitude
lngDECIMAL(9,6)GPS longitude
Partition by: DATE(timestamp)

Geospatial Indexing

GeoHash: Encode (lat, lng) thành string.

  • Precision 6: ~1.2km x 600m.
  • Precision 8: ~38m x 19m.
# Redis Geo commands
GEOADD drivers:online -122.4194 37.7749 driver_123
GEORADIUS drivers:online -122.4194 37.7749 5 km COUNT 10

API Design

POST /api/v1/rides/request
{
  "pickup": { "lat": 37.7749, "lng": -122.4194 },
  "dropoff": { "lat": 37.7849, "lng": -122.4094 },
  "ride_type": "uberx"
}

Response:
{
  "ride_id": "uuid",
  "estimated_fare": 15.50,
  "eta": 300
}

POST /api/v1/drivers/location
{
  "lat": 37.7749,
  "lng": -122.4194,
  "heading": 180
}

GET /api/v1/rides/{ride_id}/tracking

Response:
{
  "driver": { "name": "John", "vehicle": "Toyota Camry" },
  "location": { "lat": 37.7750, "lng": -122.4195 },
  "eta": 180
}

Data Flow

Request Ride:

  1. Rider POST /rides/request.
  2. Ride Service create ride (REQUESTED).
  3. Calculate fare (base + distance + time + surge).
  4. Dispatch Service tìm nearby drivers (5km radius).
  5. Filter available drivers, rank by distance/rating.
  6. Send ride request to top drivers (via push notification).
  7. First driver accepts → match.
  8. Update ride status (MATCHED), notify rider.

Driver Location Update:

  1. Driver app gửi location mỗi 3s qua WebSocket.
  2. Location Service receive, validate.
  3. Update Redis GeoIndex: drivers:online.
  4. Stream to Kafka topic driver-locations.
  5. LocationHistory Service consume, store in Cassandra.
  6. Real-time update to rider tracking (if active ride).

Matching Algorithm:

function findBestDriver(rideRequest):
    nearbyDrivers = geoSearch(rideRequest.pickup, radius=5km)
    availableDrivers = filter(nearbyDrivers, status=AVAILABLE)
    
    scoredDrivers = map(availableDrivers, driver => {
        distance = calculateDistance(driver, pickup)
        eta = calculateETA(driver, pickup)
        rating = driver.rating
        score = (rating * 0.4) - (eta * 0.6)
        return { driver, score }
    })
    
    return sortBy(scoredDrivers, score).first

Surge Pricing:

function calculateSurgeMultiplier(city, timestamp):
    demand = getRideRequestsLast10Min(city)
    supply = getAvailableDrivers(city)
    ratio = demand / supply
    
    if ratio > 2.0: return 2.0
    if ratio > 1.5: return 1.5
    if ratio > 1.2: return 1.2
    return 1.0

Bước 5: Bottlenecks & Tối ưu

Single Point of Failure

  • Location Service: Multiple instances với consistent hashing.
  • Database: PostgreSQL với replication, failover.
  • Dispatch: Stateless, horizontal scaling.

Scalability Bottlenecks

  • Location writes: 830k RPS → Shard theo driver_id hoặc city.
  • Geo queries: Redis cluster với geo-sharding theo city.
  • Matching: Parallelize driver search, timeout sau 2s.

Performance Optimization

  • Caching:
    • Driver locations in Redis (TTL 30s).
    • Fare estimates cached cho common routes.
  • Batching: Location updates batched mỗi 3-5s.
  • Async processing:
    • Payment processing async.
    • Rating updates async.
  • Connection pooling: Database connection pools.

Edge Cases

  • Driver offline during ride: Re-match rider với driver khác.
  • No drivers available: Queue ride, notify rider.
  • GPS inaccuracy: Snap to road, use last known good location.
  • Payment failure: Retry logic, fallback to cash.

Bước 6: Trade‑offs

Consistency vs Availability

  • CP system cho pricing và payment: Strong consistency.
  • AP system cho location tracking: Eventual consistency (delay vài giây acceptable).

Latency vs Accuracy

  • Low latency matching: Match với driver gần nhất trong 2s.
  • Better match: Wait 5-10s để tìm driver tốt hơn → trade-off.

Precision vs Cost

  • High precision (GeoHash 8): ~20m accuracy, nhiều data.
  • Lower precision (GeoHash 6): ~1km accuracy, ít data.
  • Solution: Dynamic precision based on density.

Real-time vs Batch

  • Real-time: Location streaming, instant matching.
  • Batch: Analytics, surge pricing calculation, driver incentives.

Kết luận

Ride-sharing system là distributed system phức tạp với challenges:

  • Real-time geospatial processing: Triệu location updates/giây.
  • Matching algorithm: Balance giữa latency và quality.
  • Dynamic pricing: Supply/demand balancing.
  • High availability: Critical cho driver/rider experience.
  • Payment integrity: Consistent, secure transactions.

← E‑commerce Platform | Xem tiếp: Video Streaming →

Case Study 6: Video Streaming (YouTube/Netflix)

Nền tảng streaming video với upload, encoding, storage, delivery, adaptive bitrate.


Bước 1: Thu thập yêu cầu

Functional requirements

  • Video upload: Users upload videos (multiple formats, sizes).
  • Video processing: Transcoding to multiple resolutions/bitrates.
  • Video streaming: Adaptive bitrate streaming (HLS/DASH).
  • Search & Discovery: Search, recommendations, trending.
  • User features: Playlists, watch history, subscriptions.
  • Comments & Likes: User interactions.
  • Live streaming: Real-time video streaming (optional).

Non‑functional requirements

  • Low latency: Video start < 2s, seek < 1s.
  • High availability: 99.9% uptime.
  • Scalability: Hàng tỷ videos, triệu concurrent viewers.
  • Bandwidth: Efficient delivery, global scale.
  • Quality: Adaptive bitrate based on network.

Scale estimation

  • Users: 2 tỷ users.
  • Videos: 10 tỷ videos.
  • Uploads per day: 500,000 hours of video.
  • Concurrent viewers: 10 triệu.
  • Bandwidth: 100 Tbps peak.

Bước 2: Ước lượng

Traffic estimates

  • Upload requests: 500k hours/day * 100 MB/min ≈ 350 PB/month.
  • Streaming requests: 2B users * 1 hour/day ≈ 2B hours/day.
  • Concurrent streams: 10 triệu.
  • Bandwidth: 10M concurrent * 5 Mbps ≈ 50 Tbps.

Storage estimates

  • Raw video: 500k hours/day * 60 min * 100 MB/min ≈ 3 PB/day.
  • Encoded video (5 renditions): 3 PB * 5 ≈ 15 PB/day.
  • Monthly storage: 15 PB * 30 ≈ 450 PB/month.
  • Total storage: Exabytes scale.

Bandwidth estimates

  • Upload: 350 PB/month ≈ 135 GB/s.
  • Download: 2B hours/day * 5 Mbps ≈ 115 Tbps (peak).
  • CDN cache hit: 90% → Origin bandwidth ≈ 11.5 Tbps.

Bước 3: Thiết kế High‑Level

Components chính

┌──────────┐     ┌─────────────┐     ┌──────────────┐
│  Client  │ ──→ │ CDN         │ ──→ │ Edge Server  │
│ (Web/Mob)│     │ (CloudFront)│     │              │
└──────────┘     └─────────────┘     └──────────────┘
                                            │
                                    ┌───────┴───────┐
                                    ▼               ▼
                            ┌───────────────┐ ┌───────────────┐
                            │   Streaming   │ │   API         │
                            │   Service     │ │   Service     │
                            └───────────────┘ └───────────────┘
                                    │               │
                                    ▼               ▼
                            ┌───────────────┐ ┌───────────────┐
                            │ Object Store  │ │   PostgreSQL  │
                            │ (S3)          │ │   (Metadata)  │
                            └───────────────┘ └───────────────┘
                                    ▲
                                    │
                            ┌───────────────┐
                            │  Transcoding  │
                            │  Pipeline     │
                            └───────────────┘
                                    ▲
                                    │
                            ┌───────────────┐
                            │   Upload      │
                            │   Service     │
                            └───────────────┘

Technology selection

  • Upload: Direct-to-S3 với presigned URLs.
  • Transcoding: AWS Elemental, FFmpeg cluster.
  • Storage: S3 cho video files, Glacier cho archive.
  • CDN: CloudFront, Akamai cho edge caching.
  • Streaming: HLS (Apple), DASH (MPEG) cho adaptive bitrate.
  • Database: PostgreSQL cho metadata, Cassandra cho analytics.
  • Search: Elasticsearch cho video search.
  • Cache: Redis cho metadata, trending.

Bước 4: Thiết kế Chi tiết

Database Schema

Table: videos

ColumnTypeDescription
video_idUUIDPrimary key
uploader_idBIGINTForeign key
titleVARCHAR(500)Video title
descriptionTEXTVideo description
durationINTDuration in seconds
statusTINYINTProcessing/Ready/Private
view_countBIGINTDenormalized count
created_atTIMESTAMPUpload time

Table: video_renditions

ColumnTypeDescription
rendition_idUUIDPrimary key
video_idUUIDForeign key
resolutionVARCHAR(10)1080p, 720p, 480p, etc.
bitrateINTBitrate in kbps
codecVARCHAR(20)H.264, H.265, VP9
s3_keyVARCHAR(500)S3 object key
file_sizeBIGINTFile size in bytes

Table: video_segments

ColumnTypeDescription
rendition_idUUIDForeign key
segment_numINTSegment number (0, 1, 2…)
s3_keyVARCHAR(500)Segment file key
durationDECIMAL(5,3)Segment duration (~10s)

Video Processing Pipeline

Upload → Transcode → Store → Distribute

1. Upload:

  • Client request presigned URL từ Upload Service.
  • Upload directly to S3 (multipart upload cho large files).
  • S3 event trigger → Transcoding Pipeline.

2. Transcoding:

  • Download raw video từ S3.
  • Generate multiple renditions:
    • 4K (2160p): 20 Mbps
    • 1080p: 5 Mbps
    • 720p: 2.5 Mbps
    • 480p: 1 Mbps
    • 360p: 0.5 Mbps
  • Split into segments (~10s each).
  • Generate manifest files (.m3u8 cho HLS, .mpd cho DASH).
  • Upload renditions + manifests to S3.

3. Storage:

  • Hot storage (S3 Standard): Popular videos.
  • Cold storage (S3 Glacier): Old/rarely accessed videos.
  • Lifecycle policies auto-transition sau 90 days.

4. Distribution:

  • CDN pull from S3 origin.
  • Edge servers cache segments.
  • TTL based on video popularity.

Adaptive Bitrate Streaming

HLS (HTTP Live Streaming):

# Master playlist (index.m3u8)
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=1920x1080
1080p/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=1280x720
720p/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1000000,RESOLUTION=854x480
480p/index.m3u8

# Media playlist (1080p/index.m3u8)
#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:10.0,
segment0.ts
#EXTINF:10.0,
segment1.ts
...

Client adaptation:

  • Client monitor bandwidth và buffer.
  • Switch rendition khi network thay đổi.
  • Seamless quality adjustment.

API Design

POST /api/v1/videos/upload
{
  "title": "My Video",
  "description": "...",
  "file_size": 1073741824,
  "duration": 600
}

Response:
{
  "video_id": "uuid",
  "upload_url": "https://s3.amazonaws.com/...?signature=xxx",
  "status": "processing"
}

GET /api/v1/videos/{id}/stream
Response:
{
  "manifest_url": "https://cdn.example.com/videos/{id}/master.m3u8",
  "duration": 600,
  "available_resolutions": ["1080p", "720p", "480p"]
}

GET /api/v1/videos/{id}
POST /api/v1/videos/{id}/like
POST /api/v1/videos/{id}/comment

Data Flow

Upload Video:

  1. Client POST /videos/upload.
  2. Create video metadata (status=PROCESSING).
  3. Return presigned S3 URL.
  4. Client upload directly to S3.
  5. S3 event → SQS message.
  6. Transcoding Service consume message.
  7. Download, transcode, upload renditions.
  8. Update video status (READY).
  9. Invalidate CDN cache.
  10. Notify user.

Stream Video:

  1. Client GET /videos/{id}/stream.
  2. API return manifest URL (CDN).
  3. Client request manifest from CDN.
  4. CDN serve from edge cache (or pull from origin).
  5. Client download segments adaptively.
  6. Track view progress, quality changes.
  7. Periodic heartbeat: update view_count, watch history.

Bước 5: Bottlenecks & Tối ưu

Single Point of Failure

  • Transcoding: Multiple workers, auto-scaling.
  • S3: Built-in redundancy (11 9s durability).
  • CDN: Multiple CDN providers (CloudFront + Akamai).

Scalability Bottlenecks

  • Transcoding: Horizontal scaling với queue-based processing.
  • CDN origin: S3 với CloudFront, 90%+ cache hit.
  • Metadata reads: Read replicas, Redis cache.

Performance Optimization

  • Edge caching: CDN cache segments tại edge.
  • Prefetching: Client prefetch next segments.
  • Parallel downloads: Download multiple segments concurrently.
  • Compression: gzip manifest files.

Cost Optimization

  • Storage tiering: Hot → Cold → Archive.
  • CDN optimization: Cache policies, compression.
  • Transcoding: Spot instances cho non-urgent jobs.
  • Regional encoding: Transcode close to upload location.

Bước 6: Trade‑offs

Consistency vs Availability

  • AP system: View count eventual consistency (delay acceptable).
  • CP system: Video availability after upload (must be consistent).

Latency vs Quality

  • Low latency: Start with low resolution, scale up.
  • High quality: Buffer more before start.
  • Solution: Adaptive bitrate balances both.

Storage vs Bandwidth

  • More renditions: Better UX, higher storage cost.
  • Fewer renditions: Less storage, worse UX for some users.
  • Solution: 5-7 renditions optimal.

HLS vs DASH

FormatProsCons
HLSWidely supported, Apple ecosystemApple-controlled, H.264 only
DASHOpen standard, codec-agnosticLess supported on iOS

Kết luận

Video streaming platform là hệ thống cực kỳ phức tạp với challenges:

  • Massive storage: Exabytes scale.
  • Bandwidth optimization: CDN, adaptive bitrate.
  • Processing pipeline: Transcoding hàng triệu videos/ngày.
  • Global delivery: Low latency worldwide.
  • Cost management: Storage, bandwidth, transcoding costs.

← Ride‑sharing | Xem tiếp: Interview Questions →

Bài tập thực hành & Câu hỏi phỏng vấn

Chuẩn bị cho các câu hỏi system design trong phỏng vấn.


Câu hỏi thường gặp

1. Design a Rate Limiter

Yêu cầu: Giới hạn 100 requests mỗi phút từ một user.

Gợi ý thiết kế:

  • Algorithm: Token bucket hoặc sliding window.
  • Storage: Redis với atomic operations.
  • Distributed: Sync rate limits across servers.
  • Granularity: Per-user, per-IP, per-API-endpoint.

2. Design a Distributed Cache

Yêu cầu: Thiết kế hệ thống cache phân tán như Redis cluster.

Gợi ý thiết kế:

  • Partitioning: Consistent hashing để distribute keys.
  • Replication: Master-slave cho fault tolerance.
  • Eviction: LRU, LFU policies.
  • Consistency: Cache invalidation strategies.

3. Design a Search Autocomplete

Yêu cầu: Gợi ý từ khóa khi user typing (Google search box).

Gợi ý thiết kế:

  • Data structure: Trie với frequency counts.
  • Storage: In-memory cho low latency.
  • Ranking: Popularity, personalization, trending.
  • Scale: Top N suggestions từ billions of queries.

4. Design a Distributed File Storage

Yêu cầu: Tương tự Google Drive, Dropbox.

Gợi ý thiết kế:

  • Storage: Object storage (S3) với chunking.
  • Sync: Block-level sync, conflict resolution.
  • Sharing: ACLs, public links.
  • Versioning: Keep multiple versions.

5. Design a Notification System

Yêu cầu: Gửi push notifications đến hàng triệu devices.

Gợi ý thiết kế:

  • Channels: Push, email, SMS, in-app.
  • Queue: Kafka cho durability.
  • Rate limiting: Avoid spamming users.
  • Personalization: User preferences, timezone.

6. Design a Web Crawler

Yêu cầu: Crawl billions of web pages.

Gợi ý thiết kế:

  • URL frontier: Priority queue cho URLs to crawl.
  • Politeness: Respect robots.txt, rate limiting per domain.
  • Deduplication: URL normalization, content hashing.
  • Scale: Distributed crawling, parallel processing.

7. Design an Analytics Platform

Yêu cầu: Track user events, generate dashboards.

Gợi ý thiết kế:

  • Ingestion: Kafka stream processing.
  • Storage: Columnar database (ClickHouse, Redshift).
  • Aggregation: Real-time + batch processing.
  • Query: SQL interface cho analysts.

Gợi ý trả lời

Cấu trúc câu trả lời

  1. Clarify requirements (2-3 phút)

    • Ask về functional requirements.
    • Ask về scale (users, RPS, data size).
    • Confirm non-functional requirements (latency, availability).
  2. Estimation (2 phút)

    • Calculate RPS, storage, bandwidth.
    • Identify read-heavy vs write-heavy.
  3. High-level design (5 phút)

    • Draw block diagram với major components.
    • Explain data flow.
  4. Detailed design (10 phút)

    • Database schema.
    • API design.
    • Key algorithms.
  5. Identify bottlenecks (3 phút)

    • Single points of failure.
    • Scalability issues.
    • Performance optimizations.
  6. Trade-offs (3 phút)

    • Discuss alternatives.
    • Explain why you chose certain approaches.

Tips quan trọng

  • Drive the conversation: Đừng chờ interviewer hỏi, chủ động dẫn dắt.
  • Think aloud: Giải thích suy nghĩ của bạn.
  • Ask for feedback: “Does this make sense?”
  • Be flexible: Sẵn sàng thay đổi design khi có yêu cầu mới.
  • Practice: Vẽ diagram nhanh, tính toán nhanh.

Tài liệu tham khảo

Sách

Online Resources

Practice Platforms


Kết luận

System design phỏng vấn không có đáp án đúng/sai duy nhất. Quan trọng là:

  • Tư duy có cấu trúc: Follow methodology.
  • Communication: Giải thích rõ ràng.
  • Trade-off analysis: Hiểu pros/cons của mỗi decision.
  • Practical knowledge: Learn from real-world systems.

Chúc bạn thành công! 🎉

← Video Streaming | Quay lại System Design Index