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ức | Công nghệ | Số chủ đề |
|---|---|---|---|
| 1 | SQL Server | T-SQL, Index, Transactions, HA | 6 |
| 2 | C#/.NET | ASP.NET Core, EF Core, Architecture | 9 |
| 3 | ReactJS | React 18+, Hooks, Redux, Next.js | 14 |
| 4 | Elasticsearch | Search, Aggregations, .NET Client | 9 |
| 5 | Work Experience | Optimizely, Pension System, Monitoring | 4 |
| 6 | Algorithms | Problem Solving, Data Structures | 2 |
| 7 | System Design | Architecture, Scalability, Case Studies | 7 |
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 |
|---|---|---|
| 1 | Nền Tảng SQL Server | T-SQL cơ bản, kiến trúc, kiểu dữ liệu, DDL, DML, Joins |
| 2 | Index & Hiệu suất | Clustered/Non-Clustered Index, Execution Plans, Query Optimization |
| 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 |
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 |
|---|---|---|
| 1 | Nền Tảng C# và .NET | Ngôn ngữ C#, CLR, GC, Value vs Reference Types, Async/Await, LINQ |
| 2 | ASP.NET Core Cốt lõi | Middleware, DI, Routing, Filters, Kestrel, SignalR, gRPC |
| 3 | Xây dựng Web API | RESTful API, Authentication/Authorization, Versioning, Swagger |
| 4 | Truy cập Dữ liệu với EF Core | Code First, Migrations, N+1 Query, Transactions, Concurrency |
| 5 | Kiến trúc Phần mềm | SOLID, Design Patterns, Clean Architecture, DDD, CQRS, Event-Driven |
| 6 | Hiệu suất và Bất đồng bộ | Caching, Rate Limiting, Load Handling, Async Processing |
| 7 | Hệ thống Phân tán | Message Queue, Azure Service Bus, Docker, Kubernetes |
| 8 | Kiểm thử | Unit Test, Integration Test, xUnit, Mocking |
| 9 | Câu hỏi Phân biệt | So 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ế
- SOLID Principles — Single Responsibility, Open/Closed, Liskov, Interface Segregation, Dependency Inversion
- DRY, KISS, YAGNI — Nguyên tắc viết code sạch
- Separation of Concerns
Design Patterns
- Creational Patterns — Singleton, Factory, Builder, Prototype
- Structural Patterns — Adapter, Bridge, Composite, Decorator, Facade
- Behavioral Patterns — Observer, Strategy, Command, Mediator, Chain of Responsibility
Kiến trúc Ứng dụng
- Clean Architecture — Domain-centric layered architecture
- Hexagonal Architecture — Ports & Adapters pattern
- Onion Architecture — Layered với dependency inversion
- Domain-Driven Design (DDD) — Aggregate, Entity, Value Object, Domain Events
- CQRS — Command Query Responsibility Segregation
- Event-Driven Architecture — Event bus, Pub/Sub
- Event Sourcing — Storing state as a series of events
Kiến trúc Hệ thống
- Microservices — Service decomposition, API Gateway, Service Mesh
- Monolithic Architecture
- Serverless Architecture — Azure Functions, AWS Lambda
Phương pháp Phát triển
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 |
|---|---|---|
| 1 | JSX & Rendering | JSX syntax, Virtual DOM, Conditional rendering, List rendering |
| 2 | Components & Props | Functional components, Props, Children, Composition |
| 3 | Hooks Cơ bản | useState, useEffect, useRef, useId |
| 4 | Hooks Nâng cao | useReducer, useMemo, useCallback, useLayoutEffect, Custom Hooks |
| 5 | Context API | createContext, useContext, Provider pattern |
| 6 | Redux & Redux Toolkit | Store, Slice, Thunk, RTK Query |
| 7 | React Query | Data fetching, Caching, Mutations, Pagination |
| 8 | React Router | Routes, Navigation, Nested routes, Protected routes |
| 9 | Forms & Validation | Controlled forms, React Hook Form, Zod validation |
| 10 | Styling | CSS Modules, styled-components, Tailwind CSS |
| 11 | Performance | Memo, Code splitting, Lazy loading, Profiler |
| 12 | Testing | Jest, React Testing Library, Mock API |
| 13 | React Patterns | HOC, Render Props, Compound Components, Portals |
| 14 | Next.js Cơ bản | SSR, 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 |
|---|---|---|
| 1 | Concepts Cơ bản & Kết nối | Index, Shard, Replica, DI setup trong ASP.NET Core |
| 2 | Mapping & Field Types | Attribute mapping, Fluent mapping, text vs keyword, nested |
| 3 | Indexing Documents | CRUD, Bulk API, Upsert, Ingest Pipeline |
| 4 | Basic Search | Match, Term, Range, Bool query, Sorting |
| 5 | Query DSL Nâng cao | Multi-match, Fuzzy, Nested query, Highlight, Scroll |
| 6 | Aggregations | Metric, Terms, Range, Date Histogram, Faceted Search |
| 7 | Analyzers & Tokenizers | Built-in analyzers, Custom analyzer, Autocomplete |
| 8 | Performance Tuning | Filter vs must, Source filtering, Search After, ILM |
| 9 | Cluster Management | Health check, ILM, Alias, Snapshot, ASP.NET Core integration |
5. Work Experience
Real-world projects and professional experience in enterprise software development.
| # | Project | Role | Key Achievements |
|---|---|---|---|
| 1 | KF Project - Optimizely | Fullstack Developer | Google Maps integration, CMS configuration, cross-functional Agile team |
| 2 | SKCC Project - FPT | Backend Developer | WPF monitoring app, webhook engine, REST APIs, ElasticSearch integration |
| 3 | PTG.PPPlus3 - Pension System | Backend Developer | Report generation (-50% time), calculation optimization (-90% time), message queues |
| 4 | Interview Q&A | — | Common 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.
| # | Topic | Description |
|---|---|---|
| 1 | Linear Equation Solver | Solve ax + b = 0 equations |
| 2 | Best Time to Buy/Sell Stock | Maximize profit from stock price array |
7. System Design
Scalable system design principles, architectural patterns, and real-world case studies.
| # | Topic | Description |
|---|---|---|
| 1 | Basic Principles | Scalability, reliability, availability, efficiency |
| 2 | System Components | Load balancers, caches, databases, message queues |
| 3 | Architectural Patterns | Monolith, microservices, event-driven, serverless |
| 4 | Design Methodology | Requirements gathering, estimation, component design |
| 5 | Processing Techniques | Sharding, replication, partitioning, rate limiting |
| 6 | Case Studies | Real-world system designs: |
| - URL Shortener | ||
| - Chat Application | ||
| - Social Media Feed | ||
| - E-commerce Platform | ||
| - Ride-sharing | ||
| - Video Streaming | ||
| 7 | Interview Questions | Common system design interview questions |
Lộ trình học gợi ý
Cho người mới bắt đầu
- C# Cơ bản → OOP → Async/Await & LINQ
- SQL Server Nền tảng → Index & Hiệu suất
- JSX & Rendering → Hooks Cơ bản
Cho .NET Developer
- SOLID Principles → Design Patterns → Clean Architecture
- EF Core → CQRS → Microservices
- Elasticsearch Core Concepts → Query DSL
Chuẩn bị phỏng vấn
- Câu hỏi Phân biệt C#/.NET
- Transactions & Concurrency
- Performance Tuning
- System Design Case Studies
- Work Experience Q&A
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
- T-SQL Cơ bản & Kiến trúc - SQL Server Engine, database files, memory architecture, T-SQL fundamentals
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)
- SQL Server là gì? Sự khác biệt giữa SQL Server instance và database là gì?
- Các file database trong SQL Server có những loại nào? (
.mdf,.ndf,.ldf) SELECT,WHERE,ORDER BY,GROUP BYkhác nhau như thế nào về mục đích sử dụng?HAVINGvàWHEREkhác nhau như thế nào?- Sự khác biệt giữa
INNER JOIN,LEFT JOIN,RIGHT JOIN, vàFULL OUTER JOIN? NULLtrong SQL Server là gì? Cách xử lý NULL vớiIS NULL,ISNULL(),COALESCE()?DISTINCTdùng để làm gì? Ví dụ thực tế?- Sự khác biệt giữa
charvàvarchar? Giữavarcharvànvarchar? int,bigint,smallint,tinyintkhác nhau ở điểm gì?PRIMARY KEYvàUNIQUEconstraint khác nhau như thế nào?NOT NULLconstraint hoạt động như thế nào?DEFAULTconstraint dùng để làm gì?- Câu lệnh
INSERT INTO...VALUEScơ bản viết như thế nào? - Sự khác biệt giữa
DELETEvàTRUNCATE TABLE? IDENTITYcolumn là gì? Cú pháp khai báo như thế nào?
Mức độ: Trung bình (Mid-level)
CASEexpression (simple và searched) hoạt động như thế nào? Ví dụ thực tế?TOP NvàOFFSET-FETCHkhác nhau như thế nào? Cái nào nên dùng cho phân trang?- Các hàm xử lý chuỗi phổ biến:
LEN,SUBSTRING,CHARINDEX,REPLACE,TRIM? - Sự khác biệt giữa
GETDATE(),GETUTCDATE(),SYSDATETIME(), vàSYSUTCDATETIME()? DATEADD,DATEDIFF,DATEPARTdùng như thế nào?decimalvsfloatvsmoney- khi nào dùng cái nào? Tại sao không nên dùngfloatcho tiền tệ?datetimevsdatetime2vsdatetimeoffset- sự khác biệt và khi nào dùng?CASTvsCONVERTvsTRY_CASTvsTRY_CONVERT- khác nhau như thế nào?FOREIGN KEYconstraint là gì?ON DELETE CASCADEhoạt động như thế nào?CHECKconstraint dùng để làm gì? Ví dụ thực tế?UPDATEvớiJOINviết như thế nào?DELETEvớiJOINviết như thế nào?MERGEstatement (upsert) dùng để làm gì? Cú pháp cơ bản?OUTPUTclause trong DML dùng để làm gì?Computed columnlà gì? Sự khác biệt giữa persisted và non-persisted?Schemastrong SQL Server là gì? Tại sao nên dùng schemas thay vì tất cả trongdbo?
Mức độ: Khó (Senior)
- SQL Server Buffer Pool hoạt động như thế nào? Tại sao memory management quan trọng?
PagesvàExtentstrong SQL Server là gì? Ảnh hưởng đến hiệu năng như thế nào?- Plan Cache hoạt động như thế nào? Parameter sniffing là gì và khi nào gây ra vấn đề?
Implicit conversiontrong SQL Server là gì? Tại sao có thể gây ra index scan thay vì seek?Temporal Tables(system-versioned) là gì? Use case thực tế?SEQUENCEobject so vớiIDENTITYcolumn - ưu nhược điểm?NEWID()vsNEWSEQUENTIALID()vsIDENTITY- khi nào dùng cái nào? Tác động đến index fragmentation?BULK INSERTvsINSERT INTO...SELECT- khi nào dùng cái nào cho hiệu năng tốt nhất?- Set-based operations vs row-by-row (cursor) - vì sao nên tránh cursor trong SQL Server?
nvarchar(MAX)vsnvarchar(4000)- tác động đến hiệu năng và storage?INDEXtrong DDL - Clustered vs Nonclustered, khi nào tạo index trong CREATE TABLE và khi nào tách riêng?- 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ần | Vai trò |
|---|---|
| Command Parser | Kiểm tra cú pháp T-SQL, tạo parse tree |
| Query Optimizer | Tạo execution plan dựa trên statistics và cost model |
| Query Executor | Thực thi từng bước trong execution plan |
Storage Engine
| Thành phần | Vai trò |
|---|---|
| Buffer Manager | Quản lý Buffer Pool (cache data pages trong RAM) |
| Log Manager | Ghi WAL (Write-Ahead Logging) vào transaction log |
| Access Methods | Duyệt B-tree index, heap tables |
| Lock Manager | Quản lý locking, deadlock detection |
Database Files
SQL Server tổ chức dữ liệu trên disk theo 3 loại file:
| File | Extension | Vai trò |
|---|---|---|
| Primary Data File | .mdf | File dữ liệu chính, chứa system tables và user data |
| Secondary Data File | .ndf | File dữ liệu bổ sung (filegroups khác nhau) |
| Log File | .ldf | Transaction 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 Type | Mô tả |
|---|---|
| Data | Chứa dữ liệu của heap tables |
| Index | Chứa B-tree index nodes |
| LOB | Large Object data (nvarchar(MAX), varbinary(MAX)) |
| IAM | Index Allocation Map - theo dõi pages thuộc object nào |
| PFS | Page 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ại | Mô tả |
|---|---|
| Uniform Extent | Tất cả 8 pages thuộc cùng 1 object (dùng khi table lớn) |
| Mixed Extent | Cá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:
- SQL Server đọc page từ disk → lưu vào Buffer Pool
- Lần sau đọc page đó → lấy trực tiếp từ RAM (không đọc disk)
- 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
| Instance | Database | |
|---|---|---|
| Định nghĩa | Một cài đặt SQL Server Engine | Tập hợp dữ liệu logic trong instance |
| Cấu trúc | Quản lý memory, logins, linked servers | Tables, views, stored procedures, schemas |
| Multiple | Nhiều databases trong 1 instance | Nhiều schemas trong 1 database |
| Isolation | Shared engine resources | Cross-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-FETCHyêu cầuORDER BY. Ưu tiên dùngOFFSET-FETCHthay vìTOPcho 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ểu | Bytes | Giá trị min | Giá trị max | Dùng khi |
|---|---|---|---|---|
tinyint | 1 | 0 | 255 | Cột nhỏ như status, age (0-120) |
smallint | 2 | -32,768 | 32,767 | ID nhỏ, year |
int | 4 | -2,147,483,648 | 2,147,483,647 | FK, PK cho hầu hết tables |
bigint | 8 | -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) và 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)
| Precision | Storage |
|---|---|
| 1-9 | 5 bytes |
| 10-19 | 9 bytes |
| 20-28 | 13 bytes |
| 29-38 | 17 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ểu | Bytes | Ký hiệu khoa học | Chính xác |
|---|---|---|---|
real | 4 | ~7 chữ số | Approximate |
float | 8 | ~15 chữ số | Approximate |
float(n) | 4 hoặc 8 | n = 1-53 | Approximate |
Cảnh báo:
floatvàreallà approximate - 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ểu | Bytes | Phạm vi | Chính xác |
|---|---|---|---|
money | 8 | ±922 trillion | 4 decimal places |
smallmoney | 4 | ±214,748 | 4 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ểu | Encoding | bytes/ký tự | Tối đa |
|---|---|---|---|
char(n) | Non-Unicode (ASCII) | 1 | 8,000 ký tự |
varchar(n) | Non-Unicode (ASCII) | 1 | 8,000 ký tự |
varchar(MAX) | Non-Unicode | 1 | 2 GB |
nchar(n) | Unicode (UTF-16) | 2 | 4,000 ký tự |
nvarchar(n) | Unicode (UTF-16) | 2 | 4,000 ký tự |
nvarchar(MAX) | Unicode | 2 | 2 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ểu | Bytes | Phạm vi | Độ chính xác | Timezone |
|---|---|---|---|---|
datetime | 8 | 1753-01-01 ~ 9999-12-31 | 3.33ms | Không |
smalldatetime | 4 | 1900-01-01 ~ 2079-06-06 | 1 phút | Không |
datetime2(n) | 6-8 | 0001-01-01 ~ 9999-12-31 | 100ns | Không |
date | 3 | 0001-01-01 ~ 9999-12-31 | 1 ngày | Không |
time(n) | 3-5 | 00:00:00 ~ 23:59:59.9… | 100ns | Không |
datetimeoffset(n) | 8-10 | 0001-01-01 ~ 9999-12-31 | 100ns | Có (+/-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ểu | Mô tả | Tối đa |
|---|---|---|
binary(n) | Fixed-length binary | 8,000 bytes |
varbinary(n) | Variable-length binary | 8,000 bytes |
varbinary(MAX) | Variable-length binary | 2 GB |
image | Deprecated, 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 \ To | INT | VARCHAR | DATE | FLOAT | BIT |
|---|---|---|---|---|---|
| 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ống | Khuyế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 độ GPS | FLOAT |
| Tên người, địa chỉ | NVARCHAR(n) |
| Email, username | VARCHAR(n) |
| Mã code (fixed) | CHAR(n) |
| Ngày sinh, ngày hợp đồng | DATE |
| Timestamp tạo/sửa | DATETIME2(7) |
| Thời gian với timezone | DATETIMEOFFSET |
| Flag on/off | BIT |
| Trạng thái nhỏ (< 255 options) | TINYINT |
| File, image, binary data | VARBINARY(MAX) |
| Config JSON/XML | NVARCHAR(MAX) (JSON) hoặc XML |
| Audit/concurrent control | ROWVERSION |
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
DELETE | TRUNCATE | |
|---|---|---|
| WHERE clause | ✅ Có thể lọc | ❌ Không, xóa tất cả |
| Transaction log | Ghi từng row (slow) | Chỉ ghi deallocations (fast) |
| Triggers | Kích hoạt DML triggers | Không kích hoạt |
| Identity reset | Giữ nguyên | Reset về seed |
| Rollback | ✅ Có thể | ✅ Có thể (trong transaction) |
| Foreign Keys | Bị giới hạn bởi FK | Lỗi nếu có FK references |
| Permissions | DELETE permission | ALTER 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ùng | Khi nào dùng | Lưu ý |
|---|---|---|
EXISTS | Subquery bảng lớn, chỉ cần kiểm tra tồn tại | Dừng sớm khi tìm thấy match |
IN | Tập giá trị nhỏ/tĩnh | Có thể chậm nếu subquery trả nhiều hàng |
JOIN | Cần lấy dữ liệu từ bảng join | Optimizer thường tối ưu tốt nhất |
NOT IN | Tránh dùng khi có NULL | NULL trong tập hợp khiến toàn bộ kết quả FALSE |
NOT EXISTS | Anti-join an toàn với NULL | Khuyế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ỏ duplicateUNION ALLlà 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:
- TVF với tham số từ outer row:
CROSS APPLY dbo.fn(e.EmployeeId)- JOIN không thể làm điều này. - 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. - 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:
OPTION (RECOMPILE)- compile lại mỗi lần (mất cache benefit)OPTION (OPTIMIZE FOR UNKNOWN)- dùng average statistics- Local variable trick -
DECLARE @Local = @Param - 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:
- Sparse data: cột có nhiều NULL (chỉ index hàng không NULL)
- Soft deletes:
WHERE IsDeleted = 0- chỉ index active records - Status filtering: chỉ query một trạng thái cụ thể thường xuyên
- 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_recompilehoặcOPTION (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:
- Rows mới đi vào Delta Store (row-store B-tree nhỏ trong TempDB/data file)
- Delta Store tích lũy đến ~1 triệu rows
- Tuple Mover (background process) compress và chuyển Delta Store → Column Segments (read-only, highly compressed)
- 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:
- Cardinality estimate quá thấp (statistics lỗi thời) → cấp ít memory grant
- Query thực sự xử lý dữ liệu rất lớn
Giải pháp:
- Update statistics
- Thêm/sửa indexes để reduce rows trước khi join
- Tăng memory grant hint:
OPTION (MIN_GRANT_PERCENT = 50) - Tăng server memory
- 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:
- Data distribution lệch (skewed) - histogram chỉ có 200 steps
- Nhiều NULL values
- Correlated columns (stats độc lập nhau, không track correlation)
- 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
REORGANIZEhoặ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:
- 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;
-
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)
-
Check statistics:
sys.dm_db_stats_properties- xem modification_counter cao → update statistics -
Check blocking/locks:
sys.dm_exec_requestsvới blocking_session_id -
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? -
Quick fix:
EXEC sp_recompile 'ProcName'hoặc force plan qua Query Store -
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ác | Mô tả | Chi phí |
|---|---|---|
| Index Seek | Đi qua B-tree để đến đúng leaf page | O(log N) - rất nhanh |
| Index Scan | Đọc toàn bộ leaf pages của index | O(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ự |
| Insert | Nhanh (thêm vào bất kỳ đâu) | Có thể gây page split |
| Range scan | Chậm (random I/O) | Nhanh (sequential I/O) |
| Lookup từ NCX | RID lookup (kém hơn) | Key lookup |
| Khi nào dùng | Staging tables, bulk insert | Hầ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 |
| Unique | Nếu không unique, SQL Server tự thêm 4-byte uniquifier |
| Ever-increasing | INSERT luôn vào cuối → tránh page split → IDENTITY hoặc NEWSEQUENTIALID() |
| Static | Cậ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:
- SQL Server phải tạo page mới
- Di chuyển ~50% dữ liệu sang page mới
- 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 Factor | Dùng khi | Hệ quả |
|---|---|---|
| 100 (0) | Read-only tables | Tốn ít space, nhiều page split nếu có insert |
| 80-90 | Mixed workload | Cân bằng space và performance |
| 60-70 | Heavy insert/update vào giữa | Nhiề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:
- Có 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
| REORGANIZE | REBUILD | |
|---|---|---|
| Fragmentation | < 30% | > 30% |
| Lock | Online (không block) | Offline hoặc ONLINE option |
| Log usage | Ít hơn | Nhiều hơn |
| Statistics | Không update | Update (FULLSCAN) |
| Fill factor | Dùng fill factor hiện tại | Có 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 case | Ví dụ |
|---|---|
| Sparse data | Cột có nhiều NULL (chỉ index hàng có giá trị) |
| Soft deletes | WHERE IsDeleted = 0 |
| Status filtering | WHERE Status = 'Active' hoặc 'Pending' |
| Recent data | WHERE CreatedDate >= '2024-01-01' |
| Partial uniqueness | Unique 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ểm | Row-store | Columnstore |
|---|---|---|
| Lưu trữ | Theo hàng | Theo cột |
| Compression | Thấp (~1x) | Cao (5-10x điển hình) |
| Point queries | Tốt | Kém |
| Aggregation trên nhiều hàng | Chậm | Rất nhanh (batch mode) |
| Write performance | Tốt | Kém hơn (delta store) |
| Phù hợp | OLTP | OLAP, 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
| LIKE | Full-Text | |
|---|---|---|
| Pattern matching | '%keyword%' (leading wildcard = scan) | Indexed word lookup |
| Hiệu suất | Chậm với %...% | Nhanh |
| Stemming (run/runs/running) | Không | Có |
| Proximity search | Không | Có (NEAR) |
| Ranking | Không | Có (RANK) |
| Setup | Không cần | Cầ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 Index | Tố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
- Equality first: Cột dùng
=đặt trước - Range last: Cột dùng
>,<,BETWEENđặt sau - Selectivity: Cột có nhiều distinct values → seek hiệu quả hơn
- 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 Index | Index Intersection | |
|---|---|---|
| Cần | 1 index với tất cả cột cần | Nhiều indexes |
| Hiệu suất | Thường tốt hơn | Có thêm cost merge |
| Flexibility | Index cụ thể cho query | Indexes có thể dùng nhiều query |
| Khi nào optimizer chọn | Khi covering index tồn tại | Khi 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 Index | Range Index (Nonclustered) | |
|---|---|---|
| Point lookup | O(1) | O(log N) |
| Range scan | ❌ Không tốt | ✅ Tốt |
| ORDER BY | ❌ Không | ✅ Có |
| Phù hợp | Cache, session lookups | Queries 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 Loops | Hash Join | Merge Join | |
|---|---|---|---|
| Yêu cầu | Index trên inner | Memory cho hash table | Sorted inputs |
| Tốt nhất | Outer nhỏ + inner indexed | Large unsorted inputs | Large sorted inputs |
| Memory | Thấp | Cao (có thể spill) | Thấp (nếu đã sorted) |
| Parallelism | Hạn chế | Tốt | Tố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 Table | Table Variable | |
|---|---|---|
| Statistics | Có (accurate cardinality) | Không (luôn ước tính 1 row) |
| Indexes | Tạo được | Chỉ inline indexes |
| Recompile on populate | Có thể (nếu stats thay đổi) | Không |
| Transaction | Tham gia transaction bên ngoài | Không bị rollback |
| Scope | Session / nested scopes | Batch |
| TempDB usage | Có | Có (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:
- Estimated cost >
cost threshold for parallelism(mặc định: 5) MAXDOPkhông = 1- 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
| Workload | MAXDOP |
|---|---|
| OLTP heavy | 1 (tránh parallel overhead) |
| Mixed workload | 4-8 |
| OLAP/DWH | 0 (tất cả cores) hoặc số cores per NUMA node |
| Reporting queries | HIGH (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 Type | Nguyên nhân | Giải pháp |
|---|---|---|
PAGEIOLATCH_SH/EX | I/O chờ đọc/ghi page | Thêm RAM (buffer pool), optimize query, SSD |
LCK_M_X | Lock contention | Optimize transactions, shorter TX scope |
CXPACKET | Parallel query sync | Giảm MAXDOP, tăng cost threshold |
ASYNC_NETWORK_IO | Client đọc kết quả chậm | Giảm result set, pagination |
SOS_SCHEDULER_YIELD | CPU pressure | Thêm CPU, optimize CPU-heavy queries |
WRITELOG | Transaction log I/O | Faster 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/Profiler | Extended Events | Query Store | |
|---|---|---|---|
| Overhead | Cao | Thấp | Rất thấp |
| Real-time | Có | Có | Delayed |
| History | Không (file only) | Không (ring buffer) | Có (persistent) |
| Plan tracking | Không | Có (với action) | Có (automatic) |
| Plan forcing | Không | Không | Có |
| Khuyến nghị | Không dùng mới | Troubleshooting | Default 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ần | Mô tả |
|---|---|
| Header | Tên table, tên index/column, thời gian update |
| Density Vector | Thống kê tính selectivity của column combinations |
| Histogram | Phâ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 stepRANGE_ROWS: Số rows có giá trị trong range (không bao gồm boundary)EQ_ROWS: Số rows có giá trị bằngRANGE_HI_KEYDISTINCT_RANGE_ROWS: Số distinct values trong rangeAVG_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 ONgiú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 STATISTICS | sp_updatestats | |
|---|---|---|
| Phạm vi | Một table/statistics | Toàn bộ database |
| Điều kiện | Luôn update | Chỉ update nếu có modification |
| Control | Tùy chọn FULLSCAN/SAMPLE | Dùng default sample rate |
| Sử dụng | Maintenance schedule | Quick 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) | |
|---|---|---|
| Introduced | SQL Server 7.0 | SQL Server 2014 |
| Compat Level | ≤ 110 | ≥ 120 |
| Multi-predicate | Independence assumption | Exponential backoff |
| JOIN estimation | Simple formula | More sophisticated |
| Ascending keys | Estimate 1 row | Better 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
- Execution Plan: Nhìn vào “Estimated Number of Rows” vs “Actual Number of Rows”
- Nếu ratio > 10x: Có thể có cardinality estimation problem
- 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:
- Update statistics thường xuyên hơn (sau batch insert lớn)
- Dùng Trace Flag 2389/2390 (SQL 2014 trở về)
- Nâng Compatibility Level ≥ 130 (CE 120 xử lý tốt hơn)
- Dùng Filtered Statistics cho data mới
- 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:
- 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);
-
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.
-
Monitor modification counter per partition: Dùng
sys.dm_db_incremental_stats_propertiesthay vìsys.dm_db_stats_propertiescho 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()và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 FREEPROCCACHErồi chạy lại với actual parameter
Step 3 - Fix options (theo mức độ invasive):
UPDATE STATISTICS ... WITH FULLSCAN(least invasive)- Tạo Filtered Statistics hoặc Multi-column Statistics
OPTION (RECOMPILE)cho stored procedure với parameter sniffing- Query Store: Force good plan với
sp_query_store_force_plan - Nâng/hạ Compatibility Level
- 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)
- Enable “Include Actual Execution Plan” (Ctrl + M)
- Run your query
- View the Execution Plan tab
Using T-SQL
SET STATISTICS XML ON;
-- Your query here
SET STATISTICS XML OFF;
Common Operators
| Operator | Description |
|---|---|
| Table Scan | Reads all rows from a table |
| Index Scan | Reads all entries from an index |
| Index Seek | Uses index to find specific rows |
| Clustered Index Scan | Scans entire clustered index (entire table) |
| Nested Loops | Joins using nested iteration |
| Hash Join | Joins using hash table |
| Merge Join | Joins using sorted inputs |
| Compute Scalar | Calculates new values from existing data |
| Sort | Orders data |
| Filter | Filters 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:
| Metric | Value | Assessment |
|---|---|---|
| I/O Cost | 0 | No disk reads |
| CPU Cost | 0.001982 | Very small |
| % Cost | 2% | Not a bottleneck |
| Executions | 1 | No 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
| Parameter | Your Value | Meaning |
|---|---|---|
| Operator Cost | 0.001982 (2%) | ✅ No concern, only 2% of query |
| I/O Cost | 0 | ✅ No disk reads, CPU only |
| Estimated Rows | 19,820 | ⚠️ Need to compare with Actual Rows |
| Executions | 1 | ✅ No loop |
| Row Size | 56 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:
| Metric | Value | Percentage | Assessment |
|---|---|---|---|
| Operator Cost | 0.113973 | 97% | ❌ Very high |
| I/O Cost | 0.0920139 | 81% of operator | ❌ Reading from disk |
| CPU Cost | 0.021959 | 19% 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
| Parameter | Value | Meaning |
|---|---|---|
| Executions | 1 | Runs only once (not in a loop) ✅ |
| Rebinds | 0 | Not re-initializing parameters ❌ |
| Rewinds | 0 | No loop join rewind ✅ |
| Ordered | False | Result 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
| Operator | Cost | I/O Cost | CPU Cost | Rows |
|---|---|---|---|---|
| Clustered Index Scan | 0.113973 (97%) | 0.092014 | 0.021959 | 19,820 |
| Compute Scalar | 0.001982 (2%) | 0 | 0.001982 | 19,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:
| Metric | Value | Assessment |
|---|---|---|
| Actual vs Estimated Rows | 19,820 = 19,820 | Statistics accurate |
| Executions | 1 | No loop |
| Row Size | 47 B | Small data |
❌ Points to Improve:
| Metric | Value | Issue |
|---|---|---|
| Operator Cost | 97% | Takes almost entire cost |
| I/O Cost | 0.092 | Reading disk, not cache |
| Storage | RowStore | Appropriate but scanning all |
Questions to Determine If Optimization is Needed
-
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
-
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
-
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:
- View query text (F4 or hover over operator) to see WHERE clause
- Check total rows in the table
- If there’s a valid WHERE clause → create non-clustered index
- If query actually runs slow (> 500ms) → optimize now
- 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 Procedures | Thủ tục lưu trữ, tham số, dynamic SQL, error handling, transactions |
| User-Defined Functions | Scalar, Inline TVF, Multi-Statement TVF, performance considerations |
| Triggers | DML/DDL/Logon triggers, inserted/deleted tables, anti-patterns |
| Views, CTEs & Window Functions | Views, 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ống | Giải pháp tốt nhất |
|---|---|
| Tái sử dụng business logic phức tạp | Stored Procedure |
| Tính toán trong SELECT, tái sử dụng | Inline TVF hoặc Scalar UDF (SQL 2019+) |
| Tự động audit/validate khi DML | Trigger (thận trọng) |
| Đơn giản hóa truy vấn phức tạp | View hoặc CTE |
| Aggregation theo partition | Window Functions |
| Traversal hierarchy | Recursive CTE |
| Pre-aggregate dữ liệu lớn | Indexed 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 Function | Table-Valued Function |
|---|---|---|
| Trả về | Một giá trị đơn | Một tập kết quả (table) |
| Dùng trong | SELECT, WHERE, JOIN | FROM clause |
| Performance | Chậ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 ý:
@@ERRORbị reset sau mỗi câu lệnh. DùngTRY...CATCHthay 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:
- Parameterized: Ngăn SQL injection
- Plan caching: Cùng SQL text → tái sử dụng execution plan
- 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:
WITH SCHEMABINDINGtrong CREATE VIEW- Unique Clustered Index là index đầu tiên tạo trên view
- Các function phải deterministic
- Không dùng
*,DISTINCT,TOP,OUTER JOIN, subqueries, CTEs SET ANSI_NULLS ONvàSET 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 Trigger | INSTEAD OF Trigger | |
|---|---|---|
| Thời điểm | Sau khi DML hoàn thành | Thay 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ên | Tables | Tables và Views |
| Dùng cho | Audit, cascade business rules | Updatable 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:
| CTE | Temp Table | Table Variable | |
|---|---|---|---|
| Scope | Trong query | Session | Batch/SP |
| Statistics | Không có | Có (auto) | Không có (trước 2019) |
| Index | Không | Có thể tạo | Chỉ PK/UK |
| Reuse nhiều lần | Không (re-evaluate) | Có | Có |
| Transaction log | Không riêng biệt | TempDB | TempDB |
| Lớn hơn 1000 rows | Nê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:
| RAISERROR | THROW | |
|---|---|---|
| Cú pháp | RAISERROR(msg, severity, state) | THROW [error_number, message, state] |
| Severity | Cần chỉ định | Mặc định 16 |
| Re-throw | RAISERROR với @ErrorMessage | THROW; (không tham số) |
| Error number tùy chỉnh | Cần trong sys.messages | Bất kỳ số >= 50000 |
| SQL version | Cũ | SQL 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, inserted và deleted 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:
- Chỉ có một
RETURNstatement - Không dùng
EXECUTE - Không dùng recursive calls
- Không dùng
TRY...CATCH - Không dùng table variables
- Không có side effects (chỉ SELECT)
- 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ạiRANGE: Running total bao gồm tất cả rows cùng Date
Performance:
RANGE BETWEENyêu cầu spool operator (lưu tạm kết quả) → chậm hơnROWS BETWEENhiệ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ùngROWS
Q25: CDC (Change Data Capture) vs Triggers: khi nào dùng cái nào?
A:
| Triggers | CDC | |
|---|---|---|
| Implementation | T-SQL manual | SQL Server built-in |
| Overhead | Synchronous, trong transaction | Asynchronous, đọc transaction log |
| Impact on DML | Có (tăng latency) | Minimal (async) |
| Granularity | Row + column level tùy chọn | Row + column level |
| History | Phải tự build | Built-in change table |
| Setup | Đơn giản | Phức tạp hơn |
| Use case | Real-time validation/cascade | Audit, 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 SavepointNamechỉ undo đến savepoint, không kết thúc transaction@@TRANCOUNTkhô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
@@NESTLEVELtrả 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:
- Gọi Stored Procedure cho mỗi row (không thể set-based)
- Cần gửi kết quả row-by-row qua network stream
- 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:
- SQL text được hash → tìm trong plan cache
- Nếu tìm thấy → reuse (soft parse)
- 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ọiDBCC FREEPROCCACHEđược chạy- Bộ nhớ áp lực cao (cache eviction)
- Schema của referenced objects thay đổi
SEToptions 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 ích | Giải thích |
|---|---|
| Code Reuse | Viết một lần, gọi từ nhiều nơi |
| Security | Cấp quyền EXECUTE thay vì trực tiếp trên tables |
| Performance | Execution plan được compile và cache |
| Reduced Network Traffic | Chỉ truyền tên SP + tham số |
| Maintainability | Thay đổ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 conditionkhô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 Practice | Lý do |
|---|---|
SET NOCOUNT ON | Giảm network traffic |
SET XACT_ABORT ON | Auto rollback an toàn |
| Validate parameters đầu vào | Fail 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 SQL | Ngăn SQL injection, plan reuse |
Tránh SELECT * | Explicit columns tốt hơn |
Dùng THROW thay RAISERROR | Modern, giữ context tốt hơn |
| Comment business logic phức tạp | Maintainability |
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áp | Một SELECT, không BEGIN/END | Có BEGIN/END, khai báo @Table |
| Optimizer | Có thể “inline” (mở rộng như view) | Black box - optimizer không nhìn thấy bên trong |
| Statistics | Dùng base table statistics | Không có statistics trên @Table variable |
| Parallelism | Được | Không được (trước SQL 2022) |
| Complexity | Đơn giản | Phức tạp hơn |
| Use case | Mọ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ỏi | Gợ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)
| Event | inserted | deleted |
|---|---|---|
| INSERT | Rows được INSERT | Trống |
| DELETE | Trống | Rows bị DELETE |
| UPDATE | Rows 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?
- Synchronous: Trigger chạy trong cùng transaction với DML gốc
- Blocking: DML phải chờ trigger hoàn thành mới commit
- inserted/deleted tables: Phải tạo copy của data vào tempdb
- Locks: Transaction dài hơn → lock lâu hơn → deadlock potential
- 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?
| Scenario | Dù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ọng | Constraints, Application logic |
| Cascade delete/update | ✅ | FK với CASCADE |
| Tính toán derived fields | ❌ Tránh | Computed columns |
| Sync giữa tables | ⚠️ | Application logic, Service Bus |
| Updatable views | ✅ INSTEAD OF | N/A |
| Prevent DDL changes | ✅ DDL Trigger | DENY permissions |
| Chặn specific users | ✅ Logon Trigger | DENY 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ộc | Mô 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ầu | Chi tiết |
|---|---|
WITH SCHEMABINDING | Bắt buộc |
| First index là UNIQUE CLUSTERED | Bắt buộc |
Không có * | Phải explicit columns |
Không DISTINCT, TOP, subqueries | |
Không OUTER JOIN | Chỉ INNER JOIN |
COUNT_BIG(*) thay vì COUNT(*) | Khi có GROUP BY |
| Deterministic functions only | |
SET ANSI_NULLS ON và SET 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
| CTE | Subquery | Temp 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
| Type | Mô tả | Use Case |
|---|---|---|
| FAST_FORWARD | Read-only, forward-only, nhanh nhất | Khi chỉ cần đi qua một lần |
| STATIC | Snapshot data vào tempdb, data cố định | Khi data có thể thay đổi trong khi đọc |
| KEYSET | Keys được lưu, data rows được đọc lại | Biến đổi data visible, insert không |
| DYNAMIC | Phản ánh mọi thay đổi real-time | Khi 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ật | Dùng khi | Tránh khi |
|---|---|---|
| View | Simplify complex queries, security layer | Cần parameters (dùng TVF thay) |
| Indexed View | Pre-aggregate large fact tables | Bảng hay INSERT/UPDATE (overhead cao) |
| CTE | Readable queries, recursive, DELETE/UPDATE | Dataset lớn cần index (dùng temp table) |
| Window Function | Ranking, running totals, lag/lead | Không có ORDER BY trong OVER() (cho một số functions) |
| Cursor | Gọi SP từng row, không thể set-based | Mọ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 Levels | Dirty Read, Phantom Read, RCSI, Snapshot Isolation, NOLOCK |
| Locking, Blocking & Deadlocks | Lock 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ểm | READ COMMITTED | RCSI |
|---|---|---|
| Cơ chế | Shared locks | Row versioning (tempdb) |
| Readers block Writers | Có | Không |
| Writers block Readers | Có | Không |
| Dirty Read | Không | Không |
| Overhead | Lock overhead | Version store trong tempdb |
| Mặc định Azure SQL | Không | Có |
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:
- Đầu tiên acquire U lock để đọc row (scan)
- 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ân | Isolation ngăn chặn |
|---|---|---|
| Non-repeatable Read | Row bị UPDATE/DELETE | REPEATABLE READ trở lên |
| Phantom Read | Row mới bị INSERT | SERIALIZABLE 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:
- 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.
- Khi COMMIT, SQL Server chỉ cần đảm bảo log records được flush to disk (không cần data pages).
- 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:
- 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.
- Blocking: Giữ locks lâu dài, blocking các session khác.
- Version Store bloat (nếu dùng RCSI/SI): Row versions phải giữ trong tempdb cho đến khi transaction kết thúc.
- 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:
- 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.
- 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:
| RCSI | SNAPSHOT ISOLATION | |
|---|---|---|
| Kích hoạt | ALTER DATABASE ... SET READ_COMMITTED_SNAPSHOT ON | ALTER DATABASE ... SET ALLOW_SNAPSHOT_ISOLATION ON |
| Áp dụng cho | Tất cả READ COMMITTED queries (tự động) | Chỉ sessions SET TRANSACTION ISOLATION LEVEL SNAPSHOT |
| Snapshot time | Statement-level (mỗi statement thấy snapshot khác nhau) | Transaction-level (toàn bộ transaction thấy cùng snapshot) |
| Write conflicts | Không kiểm tra | Kiể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:
- 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;
- 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:
- 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.
- Giữ transaction ngắn: Giảm thời gian giữ locks.
- Thêm đúng indexes: Giảm số rows phải lock, tránh table scans.
- Dùng UPDLOCK hint: Lấy U lock từ đầu thay vì S -> X conversion.
- Dùng RCSI/SNAPSHOT: Readers không lấy S locks, loại bỏ Read-Write deadlocks.
- 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:
- 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
-
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.
-
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 + 1COMMIT→@@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 SAVEPOINTkhô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:
- REDO (Roll forward): Áp lại tất cả log records của committed transactions sau checkpoint
- 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:
- Prepare Phase: MSDTC hỏi tất cả participants “bạn sẵn sàng commit chưa?”
- 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ệm | Mô tả |
|---|---|
BEGIN TRAN | Mở transaction tường minh |
COMMIT | Xác nhận và lưu vĩnh viễn |
ROLLBACK | Hủy toàn bộ transaction |
SAVE TRAN name | Đặt savepoint để rollback một phần |
@@TRANCOUNT | Số transactions đang mở |
XACT_ABORT ON | Tự động rollback khi có lỗi |
XACT_STATE() | Trạng thái transaction trong CATCH |
| WAL | Write-Ahead Logging — đảm bảo Durability |
| Checkpoint | Flush dirty pages xuống disk |
| MSDTC | Distributed 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ệu và hiệ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 Level | Dirty Read | Non-repeatable Read | Phantom Read | Lost 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:
- Ghi phiên bản cũ của row vào Version Store trong tempdb
- 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
| Level | Cơ chế | Phù hợp khi |
|---|---|---|
| READ UNCOMMITTED | No locks | Approximate monitoring, speed critical |
| READ COMMITTED | Statement-level S locks | OLTP thông thường (default) |
| REPEATABLE READ | Transaction-level S locks | Cần re-read consistency |
| SERIALIZABLE | Key-range locks | Tài chính, inventory check |
| RCSI | Row versioning (statement) | OLTP high concurrency (best default) |
| SNAPSHOT | Row 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
| Lock | Tên | Khi nào |
|---|---|---|
| Sch-M | Schema Modification | ALTER TABLE, DROP TABLE, rebuild index |
| Sch-S | Schema Stability | Compile 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 → | IS | S | U | IX | SIX | X |
|---|---|---|---|---|---|---|
| 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:
- DEADLOCK_PRIORITY: Session có priority thấp hơn bị chọn
- 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ến | Bật RCSI, tối ưu index, rút ngắn transactions |
| Deadlock | Consistent lock order, UPDLOCK, RCSI, retry logic |
| Lock Escalation | LOCK_ESCALATION = DISABLE, ROWLOCK hint, nhỏ batches |
| NOLOCK bừa bãi | Dùng RCSI hoặc SNAPSHOT thay thế |
| Long-running transactions | Batch processing, monitor @@TRANCOUNT |
| Không biết ai đang block | sp_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 Server | Authentication, Permissions, RLS, TDE, Always Encrypted, Audit, SQL Injection |
| Backup & Recovery | Full/Differential/Log Backup, Recovery Models, RESTORE, Point-in-Time Recovery |
| SQL Server Agent & Jobs | Scheduling, Jobs, Alerts, Operators |
| Monitoring & Diagnostics | DMVs, 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:
| Role | Quyền hạn |
|---|---|
db_owner | Full control database |
db_datareader | SELECT trên tất cả tables |
db_datawriter | INSERT/UPDATE/DELETE trên tất cả tables |
db_ddladmin | Tạo/sửa schema objects |
db_securityadmin | Quản lý permissions, roles |
db_backupoperator | Backup database |
public | Mọ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 Type | Nội dung | Recovery Model |
|---|---|---|
| Full | Toàn bộ database | Tất cả |
| Differential | Thay đổi từ lần Full backup cuối | Tất cả |
| Transaction Log | Log records từ lần log backup cuối | Full, Bulk-logged |
| File/Filegroup | Backup từng file | Full, Bulk-logged |
| Copy-only | Full backup không ảnh hưởng backup chain | Tất cả |
Q9: Recovery Models trong SQL Server là gì?
A:
| Model | Log truncation | Point-in-time Recovery |
|---|---|---|
| SIMPLE | Khi checkpoint | ❌ Không hỗ trợ |
| FULL | Khi log backup | ✅ Hỗ trợ |
| BULK_LOGGED | Khi 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:
| TDE | Always Encrypted | |
|---|---|---|
| Loại | Encryption at rest | Client-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 Server | Client/Application |
| Bảo vệ khỏi | Disk theft | DBA, 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 Type | Nghĩa |
|---|---|
LCK_M_* | Lock wait (blocking) |
PAGEIOLATCH_* | Đọc page từ disk (I/O bottleneck) |
CXPACKET | Parallel query coordination |
SOS_SCHEDULER_YIELD | CPU pressure |
ASYNC_NETWORK_IO | Client không đọc kết quả đủ nhanh |
WRITELOG | Log 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
saaccount (đặ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+):
| Role | Quyền hạn |
|---|---|
| sysadmin | Thực hiện mọi thứ — superuser |
| securityadmin | Quản lý logins, server audit specs |
| serveradmin | Cấu hình server settings, shutdown |
| setupadmin | Thêm/xóa linked servers |
| processadmin | Kill processes bất kỳ |
| diskadmin | Quản lý disk files |
| dbcreator | Tạo, alter, drop, restore databases |
| bulkadmin | Chạy BULK INSERT |
| public | Tấ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
| Role | Quyền hạn |
|---|---|
| db_owner | Full control database |
| db_securityadmin | Quản lý roles và permissions trong db |
| db_accessadmin | Thêm/xóa Windows và SQL logins |
| db_backupoperator | Backup database |
| db_ddladmin | Tạo/sửa/xóa schema objects |
| db_datawriter | INSERT, UPDATE, DELETE trên tất cả tables |
| db_datareader | SELECT trên tất cả tables |
| db_denydatawriter | DENY INSERT, UPDATE, DELETE |
| db_denydatareader | DENY SELECT |
| public | Tấ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
| Deterministic | Randomized | |
|---|---|---|
| Cùng plaintext → cùng ciphertext? | ✅ Có | ❌ Không |
| Hỗ trợ equality search | ✅ WHERE SSN = @ssn | ❌ Không |
| Hỗ trợ JOIN | ✅ Có | ❌ Không |
| Bảo mật | Thấ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
| Area | Best Practice |
|---|---|
| Authentication | Windows Auth, disable sa, strong passwords |
| Authorization | Principle of Least Privilege, custom roles per function |
| Encryption at rest | TDE cho production databases |
| Encryption in transit | Force Encrypted connections (ssl) |
| Sensitive columns | Always Encrypted hoặc DDM |
| Access control | RLS cho row-level isolation |
| SQL Injection | Parameterized queries, sp_executesql, stored procs |
| Auditing | SQL Server Audit cho login failures + sensitive data access |
| Monitoring | Regular permission reviews, alert on sysadmin changes |
| Backup | Encrypt 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 Model | Log Truncation | Transaction Log Backup | Point-in-time Recovery |
|---|---|---|---|
| SIMPLE | Automatic (checkpoint) | Không hỗ trợ | Không |
| FULL | Chỉ sau log backup | Bắt buộc | Có |
| BULK-LOGGED | Chỉ sau log backup | Có | Hạ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
| Option | Trạng thái sau restore | Khi nào dùng |
|---|---|---|
| WITH RECOVERY | Database ONLINE, nhận connections | Restore cuối cùng trong chuỗi |
| WITH NORECOVERY | Database RESTORING, không nhận connections | Khi còn cần apply thêm backup |
| WITH STANDBY | Database online read-only | Log 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áp | RPO | RTO | Chi phí |
|---|---|---|---|
| Full backup hàng tuần | ~7 ngày | Giờ | Thấp |
| Full + Diff + Log hàng giờ | ~1 giờ | 30-60 phút | Trung bình |
| Always On AG Sync | ~0 (zero data loss) | < 1 phút | Cao |
| Always On AG Async | Vài giây | < 1 phút | Cao |
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?
- Tail-log backup: backup phần log chưa backup (dùng
WITH NORECOVERYđể database vào RESTORING) - Restore full backup:
RESTORE DATABASE ... FROM DISK ... WITH NORECOVERY - Restore differential backup (nếu có):
RESTORE DATABASE ... WITH NORECOVERY - Restore log backups tuần tự: mỗi file log
WITH NORECOVERY, log cuối cùngWITH 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_TRUNCATEnế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:
- Switch từ FULL sang SIMPLE rồi back sang FULL → chain reset, phải lấy new full backup
- Log backup bị thiếu/miss → không restore qua khoảng đó được
- Database detach/attach không đúng cách
- 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 CHECKDBhoặcmsdb.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ần | Mô tả |
|---|---|
| Jobs | Tậ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 |
| Alerts | Phản hồi tự động khi có event/condition |
| Operators | Người nhận thông báo (email, pager) |
| Proxies | Credential 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)
| Subsystem | Mô tả | Yêu cầu |
|---|---|---|
TSQL | Chạy T-SQL script trong SQL Server | Database name |
SSIS | Chạy SQL Server Integration Services package | SSIS catalog hoặc file |
PowerShell | Chạy PowerShell script | PowerShell 2.0+ |
CmdExec | Chạy Windows command (exe, bat, cmd) | OS command access |
ActiveScripting | VBScript/JScript (legacy, deprecated) | — |
LogReader | Internal: Log Reader Agent cho replication | — |
Distribution | Internal: 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
| Day | Bit Value |
|---|---|
| Sunday | 1 |
| Monday | 2 |
| Tuesday | 4 |
| Wednesday | 8 |
| Thursday | 16 |
| Friday | 32 |
| Saturday | 64 |
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:
- Enable
Database Mail XPsquasp_configure - Tạo Mail Account (SMTP server, port, credentials)
- Tạo Mail Profile và gán Account vào Profile
- 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 CREDENTIAL → sp_add_proxy → sp_grant_proxy_to_subsystem → sp_grant_login_to_proxy.
Q: Giải thích các on_success_action và on_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, requestssys.dm_os_*: operating system, memory, waits, schedulerssys.dm_tran_*: transactions và lockssys.dm_io_*: I/Osys.dm_db_*: database-level metricssys.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ĩa | Giải pháp |
|---|---|---|
PAGEIOLATCH_SH/EX | Đọc/ghi trang từ disk | Thiếu RAM, I/O bottleneck, thiếu index |
LCK_M_X, LCK_M_S | Chờ lock (blocking) | Blocking chains, long transactions, thiếu index |
CXPACKET | Parallel query waits | Điều chỉnh MAXDOP, Cost Threshold for Parallelism |
SOS_SCHEDULER_YIELD | CPU pressure | CPU overload, nhiều CPU-intensive queries |
WRITELOG | Chờ log flush | Slow disk (log file), high write workload |
ASYNC_NETWORK_IO | Client đọc dữ liệu chậm | ứng dụng xử lý kết quả chậm, large result sets |
RESOURCE_SEMAPHORE | Chờ memory grant | Memory pressure, sort/hash operations lớn |
THREADPOOL | Không đủ workers | CPU pressure, too many connections |
PAGELATCH_* | In-memory page contention | Hotspot 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:
| Counter | Normal | Cảnh báo | Nghiê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/sec | Baseline | 20% trên baseline | 50% trên baseline |
| SQL Compilations/sec | < 10% của Batch Req | 10-20% | > 20% (thiếu reuse/parameterization) |
| SQL Re-Compilations/sec | < 10% của Compilations | ||
| Lock Waits/sec | 0 ideally | > 0 thường xuyên = blocking | |
| Deadlocks/sec | 0 | Any | Nhiều = design issue |
| Checkpoint Pages/sec | Varies | Cao liên tục = dirty page pressure | |
| Lazy Writes/sec | 0 | > 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 Events | SQL Server Profiler | |
|---|---|---|
| Performance overhead | Rất thấp | Cao (không dùng trên production) |
| Granularity | Rất chi tiết | Hạn chế hơn |
| Storage targets | Ring buffer, file, ETW | File, table |
| Real-time | Có | Có |
| Status | Hiệ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 Store | sys.dm_exec_query_stats | |
|---|---|---|
| Persistence | Persisted trong database | Lost khi plan evicted from cache |
| History | Configurable (days/weeks) | Chỉ plans còn trong cache |
| Plan comparison | Có thể compare nhiều plans per query | Một plan per entry |
| Force plan | Có | Không |
| Overhead | Nhỏ | 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?
- 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
- sys.dm_exec_query_stats: xem plan_generation_num (số lần recompile), execution count, avg duration
- Extended Events: capture query_post_execution_showplan để xem actual plan
- Wait stats per query: dùng
SET STATISTICS IO, TIME ONđể reproduce - Parameter sniffing: thử
OPTION (RECOMPILE)hoặcOPTION (OPTIMIZE FOR UNKNOWN) - Statistics: kiểm tra last_updated của statistics trên các tables liên quan — outdated stats → bad estimates → bad plan
- 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) MAXDOPper 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ăng | Mô tả | File |
|---|---|---|
| Partitioning | Chia dữ liệu bảng theo partition key, partition elimination, switching | partitioning.md |
| JSON & XML | Lưu trữ, truy vấn, export dữ liệu JSON/XML | json-xml.md |
| High Availability | Always On AG, FCI, Log Shipping, Mirroring, Replication | high-availability.md |
| Backup & Recovery | Recovery models, backup types, restore, RPO/RTO | backup-recovery.md |
| Monitoring | DMVs, Query Store, Extended Events, Wait Stats | monitoring-diagnostics.md |
| Temporal Tables | System-versioned tables, history tracking | Xem bên dưới |
| Full-Text Search | Full-text indexes, predicates CONTAINS/FREETEXT | Xem bên dưới |
| In-Memory OLTP | Memory-optimized tables, natively compiled procs | Xem bên dưới |
| Change Data Capture | CDC, Change Tracking | Xem bên dưới |
| Query Store | Plan forcing, regression detection | monitoring-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
Full-Text Search
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
| LIKE | Full-Text Search | Elasticsearch | |
|---|---|---|---|
| Wildcards | %word% | Có | Có |
| Stemming (inflections) | Không | Có | Có |
| Relevance ranking | Không | Có | Có |
| Performance | Chậm (full scan) | Nhanh (inverted index) | Rất nhanh |
| Language analysis | Không | Có | Có |
| Scalability | Hạn chế | Trung bình | Tốt |
| Complexity | Đơn giản | Trung bình | Cao |
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
| CDC | Change Tracking | |
|---|---|---|
| Data captured | Before/after values + DML type | Chỉ primary key + operation type |
| Storage | Change tables (nhiều storage) | Internal tables (ít storage) |
| History | Lâu dài (configurable retention) | Short-term (7 days thường) |
| Use case | ETL, auditing, event sourcing | Sync, incremental loads |
| Overhead | Cao hơn | Thấp hơn |
| Granularity | Row + column level | Row 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ơnFREETEXT: 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ỉ địnhBUCKET_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:
- Temporal tables cho tất cả tables chứa PII
- Retention policy: để history lâu hơn operational data (ví dụ: operational 2 năm, history 7 năm cho compliance)
- “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 - Separate history database để có thể backup/restore policy khác nhau
- Compression trên history table (ROW/PAGE COMPRESSION)
- 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:
- Dùng SWITCH cho loading (bypass CDC capture) → sau SWITCH, apply CDC capture thủ công nếu cần replication
- Đặt CDC ở downstream (sau khi data đã fully loaded)
- Dùng Change Tracking thay vì CDC nếu chỉ cần row-level tracking (Change Tracking cũng không capture SWITCH)
- 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
DELETEtố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 LEFT | RANGE 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: > 2023 | P1: < 2023, P2: ≥ 2023 |
| Thường dùng với | Date/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
| Aligned | Non-Aligned | |
|---|---|---|
| Định nghĩa | Index dùng cùng partition scheme với table | Index có partition scheme khác hoặc không partitioned |
| Partition switch | Cho phép | Không cho phép |
| Maintenance | Rebuild từng partition | Phải rebuild toàn bộ |
| Khuyến nghị | Luôn dùng aligned | Chỉ 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) = 2023→ không elimination;WHERE OrderDate >= '2023-01-01' AND OrderDate < '2024-01-01'→ có 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 OUTdata trước, rồi mớiMERGEđể 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 theo | Rows (theo giá trị partition key) | Columns (chia bảng thành nhiều bảng) |
| SQL Server | Table 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 region | Tách BLOB columns ra bảng khác |
| Use case | Scale out theo volume, data archiving | Reduce 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) = 2023khô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:
- Partition function: boundary hàng tháng (60+ boundaries cho 5 năm)
- Mỗi tháng: tạo partition mới cho tháng tới (SPLIT RANGE với NEXT USED filegroup)
- Archive tháng cũ:
- Chuẩn bị staging/archive table với correct check constraint
SWITCH OUTpartition 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
- 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_VALUE và JSON_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 array →NVARCHAR(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 controlFOR 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:
ValidFromvàValidTo - 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
| Limitation | Mô tả |
|---|---|
| TRUNCATE TABLE | Không được phép — dùng DELETE thay thế |
| Period columns | Không thể manually INSERT/UPDATE period columns qua normal DML |
| DELETE on history | Không thể DELETE từ history table trực tiếp |
| Schema changes | Cần SYSTEM_VERSIONING = OFF trước khi ALTER TABLE |
| INSTEAD OF triggers | Không hỗ trợ trên temporal tables |
| Filestream columns | Không hỗ trợ |
| In-Memory OLTP | Không kết hợp được với memory-optimized tables |
| Always Encrypted | Column-level encryption với AE có giới hạn |
| Replication | Merge replication không hỗ trợ temporal tables |
| Table partitioning | History 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 OFrấ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:
DELETE FROM dbo.TableName— xóa từng row (chậm nhưng an toàn, ghi history)- Nếu muốn xóa cả history: Tắt system versioning, TRUNCATE cả hai tables, re-enable
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:
- 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).
- Partitioning history table: Partition theo ValidFrom để aging out old data và pruning trong queries.
- Retention policy: Set
HISTORY_RETENTION_PERIODđể tự cleanup data cũ, giảm history table size. - 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:
-
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).
-
Standardized period column naming: Convention nhất quán (
AuditFrom/AuditTohoặcValidFrom/ValidTo) để automation có thể xử lý. -
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);
-
Retention tiered: Bảng financial → 7 năm. Bảng session data → 90 ngày.
-
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');
- 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;
- 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ệt | Disk-Based Tables | Memory-Optimized Tables |
|---|---|---|
| Storage | Buffer pool (disk-backed) | RAM-only (+ optional durable write-ahead log) |
| Locking | Pessimistic locking | Optimistic multi-version concurrency control (MVCC) |
| Latching | Buffer pool latches | Không có latches cho data access |
| Logging | Full transaction log | Reduced logging (chỉ insert/delete, không update-in-place) |
| Stored Procs | Interpreted T-SQL | Natively compiled → machine code |
| Indexes | B-tree on disk | Hash 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
| Option | Mô tả | Use Case |
|---|---|---|
SCHEMA_AND_DATA | Schema + data đều durable, survive restart | Permanent business data |
SCHEMA_ONLY | Schema durable, data mất sau restart | Session 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 Index | Range (Nonclustered) Index | |
|---|---|---|
| Equality (=) | Rất nhanh O(1) | OK |
| Range (<, >, BETWEEN) | Không hỗ trợ | Hỗ trợ |
| ORDER BY | Không hỗ trợ | Hỗ trợ |
| Memory usage | Cố định (bucket_count * 8 bytes) | Dynamic |
| Best for | Primary key lookups, high-frequency point queries | Range 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 Case | Lý do phù hợp |
|---|---|
| High-frequency INSERT/UPDATE | Loại bỏ lock/latch contention |
| Session state storage | SCHEMA_ONLY → nhanh, không cần persist |
| Work queues / messaging | High-throughput enqueue/dequeue |
| Real-time risk/fraud scoring | Sub-millisecond lookup |
| Temporary data aggregation | Buffer trước khi flush xuống disk-based tables |
| Hot lookup tables | Reference 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ì:
- Không cần I/O disk cho data access
- Loại bỏ locking bằng MVCC (writers không block readers)
- Không có buffer pool latches
- 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ếuavg_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_ts và end_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:
- Analysis: Dùng AMR tool hoặc DMVs để check surface area restrictions
- Create memory-optimized equivalent: Tạo table mới với
MEMORY_OPTIMIZED = ON - Dual-write pattern: Application write vào cả 2 tables trong transition period
- Backfill: Copy data từ disk-based sang memory-optimized
- Switch reads: Chuyển reads sang memory-optimized table
- Stop dual-write: Sau khi verified, bỏ writes vào disk-based table
- 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_ONLYtables: 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ệ | RPO | RTO | Readable Secondary | Transparent Failover | Chi phí |
|---|---|---|---|---|---|
| Always On AG (Sync) | ~0 (zero data loss) | < 30 giây | Có (Enterprise) | Có (với listener) | Cao |
| Always On AG (Async) | Vài giây | < 30 giây | Có (Enterprise) | Manual | Cao |
| FCI | ~0 (shared storage) | 1-5 phút | Không | Có (WSFC) | Rất cao |
| Log Shipping | Vài phút - giờ | 30-60 phút | Có (STANDBY) | Không (manual) | Thấp |
| Database Mirroring (deprecated) | ~0 (Sync) | < 30 giây | Không | Có (với witness) | Trung bình |
| Replication (Transactional) | Vài giây | Tùy | Có | Không | Trung bình |
| Azure Auto-failover Groups | Vài giây | < 1 phút | Có | Có | Cao |
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ại | Hoạt động | RPO | Use Case |
|---|---|---|---|
| Snapshot | Copy toàn bộ data định kỳ | Hours | Lookup tables, ít thay đổi |
| Transactional | Stream từng transaction | Giây | Real-time reporting, OLAP offload |
| Merge | Sync 2 chiều, conflict resolution | Phút | Mobile, distributed updates |
| Peer-to-Peer | Multi-master transactional | Giây | Geo-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 AG | FCI | |
|---|---|---|
| Scope | Database-level | Instance-level |
| Storage | Mỗi replica có storage riêng | Shared storage |
| Readable secondary | Có (Enterprise) | Không |
| License | Per-core tất cả replicas | Chỉ active nodes |
| Failover granularity | Từng database/AG | Toà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
tempdbtrên secondary → monitortempdbusage - 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:
- Secondary become Primary
- Listener routing update (< vài giây với DNN)
- 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):
- Force failover to secondary (pre-check secondary health)
- Thực hiện maintenance trên node cũ (giờ là secondary)
- Về sau failover lại về original node (nếu cần)
- 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) và 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:
cdc.YourDatabase_capture— đọc log và ghi vào change tablescdc.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/rowversionkhô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 Capture | Change Tracking |
|---|---|---|
| Before values | Có (lưu before/after) | Không |
| After values | Có | Phải query source table |
| Deleted row data | Có (before values) | Chỉ biết row bị xóa (PK only) |
| SQL Agent required | Có | Không |
| Storage overhead | Cao (full row versions) | Thấp (chỉ PK + metadata) |
| Latency | Có độ trễ (async log read) | Gần như realtime (sync) |
| Cleanup | Dựa trên retention (có SQL Agent job) | Auto cleanup (tự động) |
| Use case chính | ETL, data integration, audit | Data synchronization giữa systems |
| Memory-Optimized tables | Không hỗ trợ | Không hỗ trợ |
| Point-in-time history | Đầy đủ (mọi change) | Chỉ net change từ version |
| Complexity | Cao hơn | Thấp hơn |
| Giá thành về performance | Cao 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í | CDC | Temporal Tables |
|---|---|---|
| Mục đích chính | Data integration/ETL | Audit trail, point-in-time query |
| Before/After | Explicit (operation column) | Tự động (history table) |
| Query syntax | fn_cdc_get_all_changes | FOR SYSTEM_TIME AS OF |
| Latency | Có độ trễ (async) | Synchronous (ngay lập tức) |
| SQL Agent | Cần | Không cần |
| Cleanup | Configurable retention | Configurable HISTORY_RETENTION_PERIOD |
| DDL compatibility | Requires re-enable on schema change | Automatically handled |
| Granularity | Column-level mask | Row-level chỉ (ValidFrom/ValidTo) |
| Phù hợp nhất | ETL pipelines, downstream systems | Slowly 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 = 1khi 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:
- 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.
- Transactional checkpoint: Lưu
@to_lsnvào cùng transaction với data write vào target. Không dùng separate checkpoint store. - Dead letter queue: Với rows không thể process, ghi vào DLQ thay vì skip hay crash.
- Version tracking: Trong target table, lưu
source_lsnvàsource_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ức | Mô tả |
|---|---|---|
| 1 | Nền Tảng C# và .NET | Ngôn ngữ C#, CLR, Garbage Collection, Value Types vs Reference Types |
| 2 | ASP.NET Core Cốt lõi | Middleware, DI, Routing, Filters, Configuration |
| 3 | Xây dựng Web API | RESTful, Authentication, Authorization, Swagger |
| 4 | Truy cập Dữ liệu với EF Core | Code First, Migrations, N+1 Query, Transactions |
| 5 | Kiến trúc Phần mềm | SOLID, Design Patterns, Clean Architecture, DDD, CQRS |
| 6 | Hiệu suất và Xử lý Bất đồng bộ | Caching, Rate Limiting, Load Balancing |
| 7 | Hệ thống Phân tán | Message Queue, Docker, Kubernetes |
| 8 | Kiểm thử | Unit Test, Integration Test với xUnit |
| 9 | Câu hỏi Phân biệt | So sánh các công nghệ và concepts |
Hướng dẫn sử dụng
- Đọ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
- Thực hành: Mỗi chủ đề cần có code example đi kèm
- Ô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
- Delegates & Events - Func, Action, events, lambda expressions
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
- Async/Await - Cơ chế bất đồng bộ trong C#
- IEnumerable vs IAsyncEnumerable - So sánh synchronous và asynchronous enumeration
- LINQ - Language Integrated Query (IEnumerable vs IQueryable)
Exception Handling & IDisposable
- Exception Handling & IDisposable - Try-catch-finally, IDisposable pattern, tương tác giữa chúng
Pattern Matching, Records & Reflection
- Pattern Matching - Pattern Matching
- Records - C# 9+ Records
- Attributes & Reflection - Metadata và runtime type inspection
- Tính năng mới C# 12 - C# 12 New Features
CLR & Bộ nhớ
- Garbage Collection - Hoạt động của GC
- Thế hệ GC - Generations (Gen 0, 1, 2)
- Tối ưu GC - Cách giảm áp lực lên GC
- Value Types vs Reference Types - So sánh struct và class
- ref, out, in - Các từ khóa ref modifiers
Câu hỏi phỏng vấn (Sắp xếp từ dễ đến khó)
Mức độ: Dễ (Junior)
- Sự khác biệt giữa
value typesvàreference typestrong C#? constvàreadonlykhác nhau như thế nào?StringvsStringBuilder- khi nào nên dùng cái nào?- Sự khác biệt giữa
for,while, vàforeachloops? break,continue, vàreturnkhác nhau như thế nào?ArrayvàList<T>khác nhau gì?ref,out,inparameters khác nhau như thế nào?null-coalescing operator(??) vànull-conditional operator(?.) dùng để làm gì?
Mức độ: Trung bình (Mid-level)
abstract classvàinterfacekhác nhau như thế nào? Khi nào dùng cái nào?- Giải thích
inheritance,polymorphism, vàencapsulationtrong OOP? - Sự khác biệt giữa
delegate,event, vàlambda expression? - So sánh
List<T>,Dictionary<TKey, TValue>, vàHashSet<T>- khi nào dùng cái nào? - Khi nào nên sử dụng
generic typesvà cách áp dụng constraints? BoxingvàUnboxinglà gì? Tại sao nên tránh?Deferred Executiontrong LINQ là gì?- Sự khác biệt giữa
IEnumerablevàIQueryable- khi nào dùng cái nào? - Giải thích
try-catch-finallyhoạt động thế nào?throwvàthrow exkhác nhau gì? IDisposablelà gì? Tại sao cần implement nó?usingstatement hoạt động như thế nào với IDisposable?
Mức độ: Khó (Senior)
- Giải thích
async/awaithoạt động thế nào dưới the hood? - Sự khác biệt giữa
IEnumerablevàIAsyncEnumerable? - Cách tối ưu truy vấn LINQ trên tập dữ liệu lớn?
- Garbage Collection (GC) hoạt động như thế nào?
- Các thế hệ (Generations) trong GC là gì?
- Cách tối ưu để giảm áp lực lên GC?
- 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? Reflectionlà gì và khi nào nên sử dụng? Performance considerations?Attributestrong C# dùng để làm gì? Cho ví dụ về custom attribute.- Pattern Matching trong C# 9+ có gì mới?
RecordskhácClassesnhư thế nào? Khi nào nên dùng Records?- Tương tác giữa try-catch-finally và IDisposable -
usingstatement tương đương với try-finally như thế nào? - Tại sao không nên throw exception từ Dispose method?
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
| Feature | Abstract Class | Interface |
|---|---|---|
| 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ệm | Bản chất | Vai trò |
|---|---|---|
| Delegate | Kiểu dữ liệu (type-safe function pointer) | Định nghĩa “hợp đồng” cho phương thức |
| Lambda Expression | Cú pháp (syntax) | Cách viết ngắn gọn cho anonymous method |
| Event | Cơ chế bảo vệ (wrapper) | Giới hạn truy cập delegate, chỉ cho phép += và -= |
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ống | Sử dụng |
|---|---|
| Truyền method như parameter | Func<>, Action<>, hoặc custom delegate |
| LINQ queries | Lambda expression |
| Callback không cần expose | Lambda → Delegate |
| Publisher-Subscriber pattern | Event |
| Cần giới hạn truy cập (chỉ +=/-=) | Event |
| Cần Invoke từ bên ngoài | Delegate (không dùng event) |
Lưu ý quan trọng
- Event không thể Invoke từ bên ngoài class - Đây là sự khác biệt chính so với delegate
- Lambda expression không có kiểu riêng - Nó phải được gán cho một delegate type
- 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
| Interface | Mô tả | Ví dụ |
|---|---|---|
IEnumerable<T> | Chỉ hỗ trợ iteration | LINQ queries |
ICollection<T> | Thêm Count, Add, Remove, Clear | List<T>, HashSet<T> |
IList<T> | Thêm index-based access | List<T>, arrays |
IDictionary<TKey, TValue> | Key-value pairs | Dictionary<TKey, TValue> |
IReadOnlyCollection<T> | Chỉ đọc với Count | IEnumerable<T>.ToList() |
Span và Memory - High-Performance Memory Views
Lưu ý:
Span<T>vàMemory<T>không phải là collections theo nghĩa truyền thống. Chúng là cácref structcung 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ểm | Span<T> | Collections (List<T>, T[]) |
|---|---|---|
| Kiểu | ref struct (stack-only) | Reference type hoặc array |
| Allocation | Zero heap allocation | Heap allocation |
| Lưu trữ | Không thể là field của class | Có thể là field |
| Async | Không hỗ trợ (ref struct limitation) | Hỗ trợ đầy đủ |
| IEnumerable | Không implement | Có implement |
| Performance | Cao nhất | Thấp hơn do allocation |
| Use case | Parsing, slicing, buffer manipulation | General 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ống | Khuyến nghị |
|---|---|
| Parsing string/byte buffer | Span<T> |
| Slicing array mà không copy | Span<T> |
| High-performance buffer manipulation | Span<T> |
| Cần lưu trữ trong class | Memory<T> |
| Cần sử dụng với async/await | Memory<T> |
| General purpose data storage | List<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?
- Không implement
IEnumerable<T>- Không thể dùngforeachtrực tiếp (phải dùngforeach(ref var item in span)) - Không implement
ICollection<T>- Không cóAdd,Remove,Clear - 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 - Stack-only - Không thể boxing, không thể lưu trong heap
Tóm lại:
Span<T>vàMemory<T>là 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ý
- Khi gọi method
async, thread hiện tại không bị block - Task được tạo và chạy trên Thread Pool
- Khi async operation hoàn thành, continuation được schedule lại
- Kết quả được trả về cho caller
Lưu ý quan trọng
asyncmethod luôn trả vềTask,Task<T>, hoặcvoid(chỉ dùng cho event handlers)- Không nên dùng
.Resulthoặ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
| Feature | IEnumerable | IAsyncEnumerable |
|---|---|---|
| Execution | Synchronous | Asynchronous |
| Memory | Load all | Stream |
| Performance | Chậm với large data | Tốt với large data |
| Use case | Small datasets | Large 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
- Gọi
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
| Feature | IEnumerable | IQueryable |
|---|---|---|
| Execution | In-memory | Database/Provider |
| Deferred Execution | ✅ Yes | ✅ Yes |
| Query Composition | LINQ to Objects | LINQ to Entities/SQL |
| Performance | Load all data first | Filter at database |
| Use case | Small datasets, in-memory | Database 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
-
Sử dụng IQueryable thay vì IEnumerable cho database queries
public IQueryable<Product> GetProducts() => _context.Products; -
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 -
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ại Khi nào dùng Ưu điểm Nhược điểm Offset-based Admin 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-based Infinite scroll, mobile app, real-time feed (Facebook, Twitter) Hiệu suất cao (dùng index), không bị drift Không nhảy page được, chỉ “next/prev” -
Select only needed columns
var names = products.Select(p => p.Name).ToList(); // Not full entity -
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
| Aspect | throw ex | throw |
|---|---|---|
| Stack trace | Bị reset từ vị trí throw | Giữ nguyên stack trace gốc |
| Exception object | Tạo mới (về mặt stack trace) | Giữ nguyên object gốc |
| Debugging | Khó tìm root cause | Dễ dàng trace ngược lại |
| IL Code | throw instruction | rethrow 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
-
Luôn sử dụng
usingcho IDisposable resources// ✅ Good using var connection = new SqlConnection(connectionString); connection.Open(); // ❌ Bad var connection = new SqlConnection(connectionString); connection.Open(); // Forgot to dispose! -
Không throw exception từ Dispose
public void Dispose() { try { Cleanup(); } catch (Exception ex) { Log.Error(ex); // Log, don't throw } } -
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; } } } -
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
| Attribute | Mục đích | Ví 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
- Dependency Injection Frameworks - Tìm và register services
- ORM Frameworks - Map database columns to properties
- Serialization/Deserialization - Inspect object structure
- Testing Frameworks - Tìm và chạy test methods
- 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
- Allocation: Khi object mới được tạo, nó được allocate trên Heap
- Mark Phase: GC đánh dấu tất cả objects có reference (root objects)
- Sweep Phase: Xóa các objects không có reference
- 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 │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
| Generation | Mô tả | Tần suất GC |
|---|---|---|
| Gen 0 | Short-lived objects (local variables) | Thường xuyên nhất |
| Gen 1 | Intermediate objects | Ít hơn Gen 0 |
| Gen 2 | Long-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
| Operation | Value Type | Reference Type |
|---|---|---|
| Allocation | Stack (nhanh) | Heap (chậm hơn) |
| Copy | Copy toàn bộ | Copy reference |
| Passing | Copy toàn bộ | Copy reference (4/8 bytes) |
| GC Pressure | Không | Có |
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
| Modifier | Caller initialize | Can modify | Use case |
|---|---|---|---|
ref | ✅ Yes | ✅ Yes | In-place modification |
out | ❌ No | ✅ Yes | Return multiple values |
in | ✅ Yes | ❌ No | Read-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
- Program.cs và Minimal APIs - So sánh Minimal APIs với Startup.cs truyền thống
Middleware
- Middleware là gì? - Khái niệm và cách hoạt động
- Thứ tự Middleware - Pipeline execution order
- Custom Middleware - Cách viết Middleware tùy chỉnh
Dependency Injection
- 3 loại Lifecycle - Singleton, Scoped, Transient
- Ví dụ thực tế - Khi nào dùng loại nào?
- Scoped vào Singleton - Vì sao không nên?
Routing
- Conventional Routing - MVC routing
- Attribute Routing - Web API routing
Filters
- Các loại Filter - Authorization, Resource, Action, Exception, Result
- So sánh với Middleware - Khi nào dùng filter?
Cấu hình & Logging
- appsettings.json - Configuration
- IOptions Pattern - Options pattern
- Serilog - Logging tối ưu
Xử lý lỗi
- Global Error Handling - UseExceptionHandler, UseStatusCodePages
- ProblemDetails (RFC 7807) - Standard error response
- Custom Exception Filter - IExceptionHandler implementation
- Error Handling trong Minimal APIs - TypedResults
Model Binding
- Binding Sources - FromBody, FromQuery, FromRoute, FromHeader, FromForm
- Binding Order - Thứ tự ưu tiên
- Custom Model Binding - Custom binders và providers
- Binding trong Minimal APIs - Parameter binding
Model Validation
- Data Annotations - Built-in validation attributes
- Custom Validation Attribute - Tạo custom attributes
- IValidatableObject - Self-validating models
- FluentValidation - Fluent validation library
- Validation trong Minimal APIs - Validation pattern
ActionResult Types
- IActionResult - Common action results
- ActionResult
- Generic action results - TypedResults (Minimal APIs) - Type-safe results
- Custom ActionResult - Custom result classes
Static Files & File Handling
- Serving Static Files - wwwroot, UseStaticFiles
- File Providers - Physical, Embedded, Composite
- Cache Headers - Caching static files
- File Upload - IFormFile, streaming uploads
- File Download - File results, streaming downloads
- AWS S3 Operations - Upload, download, delete files in S3
Background Tasks
- IHostedService - Background service interface
- BackgroundService - Abstract base class
- Scheduled Tasks - Cron-based scheduling
- Worker Service Template - Creating worker services
Health Checks
- Basic Health Checks - Setup and configuration
- Custom Health Checks - IHealthCheck implementation
- Health Check UI - Visual health monitoring
- Kubernetes Probes - Liveness, Readiness, Startup
Minimal APIs Advanced
- Route Groups - Grouping routes (.NET 7+)
- Endpoint Filters - Filter pipeline
- Parameter Binding - Advanced binding
- Organizing Large APIs - Best practices
Content Negotiation
- Input Formatters - JSON, XML, custom formatters
- Output Formatters - Response formatting
- JSON Configuration - Serializer options
- Custom Formatters - CSV, etc.
Output Caching
- Output Caching vs Response Caching - Server vs client caching
- Cache Policies - Policy configuration
- Cache Tags - Tag-based invalidation
- Vary By - Query, Header, Route variation
Rate Limiting
- Rate Limiting - Giới hạn requests per IP/user
- Cấu hình - AspNetCoreRateLimit setup
- Endpoint Rate Limiting - Per-action rate limiting
Kestrel Configuration
- Endpoint Configuration - HTTP, HTTPS, Unix sockets
- HTTPS Configuration - Certificates, redirection
- Request Limits - Body size, headers, timeouts
- HTTP/2 & HTTP/3 - Protocol configuration
- Reverse Proxy - When to use
Localization (i18n)
- Resource Files - .resx files structure
- IStringLocalizer - Using localizers
- View Localization - Localizing Razor views
- Data Annotations Localization - Localized validation
- Culture Providers - Query, Cookie, Header, Route
Real-time Communication
- SignalR Basics - Hub, client-server communication
- Groups & Users - Targeted messaging
- Strongly-typed Hubs - Interface-based hubs
- Scaling with Redis - Backplane configuration
gRPC
- gRPC vs REST - When to use gRPC
- Protocol Buffers - .proto files
- Streaming - Server, Client, Bidirectional
- gRPC Client - Client setup and usage
- Interceptors - Logging, error handling
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
| Aspect | Minimal APIs | Startup.cs |
|---|---|---|
| Code lines | Ít hơn | Nhiều hơn |
| Complexity | Đơn giản | Phức tạp hơn |
| Flexibility | Hạn chế | Linh hoạt |
| Testability | Khó test hơn | Dễ test hơn |
| Use case | Microservices, small APIs | Large applications |
| DI Configuration | Trong Program.cs | Trong 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
- Exception Handling - Đầu tiên để bắt exceptions
- Security (CORS, Authentication, Authorization)
- Static Files - Nếu cần
- Routing - Xác định endpoint
- Endpoints - Controller/Action
- 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
-
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)
-
Register services với interface:
// ✅ Tốt builder.Services.AddScoped<IProductRepository, ProductRepository>(); // ❌ Tránh builder.Services.AddScoped<ProductRepository>(); -
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
| Aspect | Conventional Routing | Attribute Routing |
|---|---|---|
| Definition | Tập trung trong config | Trực tiếp trên method |
| Flexibility | Linh hoạt với patterns | Chi tiết, rõ ràng |
| RESTful | Khó tạo RESTful | Dễ dàng |
| Convention | Theo quy ước đặt tên | Tùy chỉnh |
| Use case | MVC với Views | Web 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
| Constraint | Example | Description |
|---|---|---|
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:
- Static segments (e.g.,
api/products) - Route parameters có constraints
- Route parameters không constraints
- 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 Type | Implements | Runs | Use Case |
|---|---|---|---|
| Authorization | IAuthorizationFilter | Đầu tiên | Check authentication/authorization |
| Resource | IResourceFilter | Trước & sau model binding | Caching, performance tracking |
| Action | IActionFilter | Trước & sau action | Logging, validation |
| Exception | IExceptionFilter | Khi exception xảy ra | Error handling |
| Result | IResultFilter | Trước & sau result execution | Output 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
| Level | Usage |
|---|---|
Verbose | Detailed tracing |
Debug | Debugging information |
Information | General information |
Warning | Something unexpected happened |
Error | Functionality issue |
Fatal | Critical 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 Code | When to Use |
|---|---|
| 400 Bad Request | Invalid input, validation failed |
| 401 Unauthorized | Missing or invalid authentication |
| 403 Forbidden | Authenticated but no permission |
| 404 Not Found | Resource doesn’t exist |
| 409 Conflict | Business rule violation, duplicate |
| 422 Unprocessable | Valid syntax but semantic errors |
| 429 Too Many Requests | Rate limit exceeded |
| 500 Internal Server Error | Unexpected server error |
| 503 Service Unavailable | Maintenance 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ự:
- Form values - POST form data
- Route values - URL route parameters
- Query string - URL query parameters
- 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
IActionResultvàActionResult<T>khác nhau như thế nào?- Khi nào nên dùng
IActionResultvsActionResult<T>? - Các helper methods nào có sẵn trong ControllerBase?
TypedResultstrong 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
| Aspect | IActionResult | ActionResult |
|---|---|---|
| Return type | Chỉ IActionResult | Cả IActionResult và T |
| Swagger/OpenAPI | Không rõ response type | Tự động generate schema |
| Type safety | Không | Có |
| Use case | Khi cần linh hoạt | Khi 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?
wwwrootfolder 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?
IFormFilelà 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?
IHostedServicevàBackgroundServicekhá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
| Aspect | Minimal APIs | Controllers |
|---|---|---|
| Code size | Ít hơn | Nhiều hơn |
| Complexity | Đơn giản | Phức tạp hơn |
| Features | Đầy đủ cho most cases | Full MVC features |
| Filters | Endpoint Filters | Action/Result Filters |
| Model Binding | Tự động | Attribute-based |
| Testing | Khó hơn | Dễ hơn |
| Organization | Extension methods | Controllers folder |
| Use case | Small APIs, microservices | Large 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);
Culture Cookie
// 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
- Thiết kế API đúng chuẩn - REST principles
- Model Binding - Binding request data
- Model Validation - Validation với Data Annotations
Bảo mật
- Authentication - JWT, JwtBearer configuration
- Authorization - Role-based và Policy-based
- CORS - Cross-Origin Resource Sharing
Phiên bản & Tài liệu
- API Versioning - Version management
- Swagger/OpenAPI - Documentation
Tối ưu hiệu suất
- Rate Limiting - Giới hạn requests
- Output Caching - Server-side caching
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
- Client-Server - Tách biệt client và server
- Stateless - Mỗi request chứa đủ thông tin
- Cacheable - Response có thể được cache
- Uniform Interface - Sử dụng HTTP methods và status codes đúng cách
- Layered System - Có thể có nhiều layers
HTTP Methods
| Method | Purpose | Idempotent |
|---|---|---|
GET | Lấy resource | ✅ Yes |
POST | Tạo resource mới | ❌ No |
PUT | Thay thế toàn bộ resource | ✅ Yes |
PATCH | Cập nhật một phần resource | ❌ No |
DELETE | Xóa resource | ✅ Yes |
Status Codes
| Code | Meaning | Usage |
|---|---|---|
200 | OK | GET, PUT, PATCH thành công |
201 | Created | POST tạo mới thành công |
204 | No Content | DELETE thành công |
400 | Bad Request | Validation failed |
401 | Unauthorized | Chưa authenticate |
403 | Forbidden | Không có permission |
404 | Not Found | Resource không tồn tại |
500 | Internal Server Error | Server 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:
- Model Binding - Chi tiết về binding sources, custom binders
- Model Validation - Data Annotations, FluentValidation, custom validation
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
| Strategy | Pros | Cons |
|---|---|---|
| URL Path | Rõ ràng, dễ debug | Phải update client URLs |
| Query String | Không thay đổi URL | Ít visible |
| Header | Linh hoạt | Khó 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ế
- Code First vs Database First - Hai cách tiếp cận
- Migrations - Quản lý schema
- DbContext và Change Tracker - Cơ chế tracking
Tối ưu hiệu suất
- N+1 Query Problem - Vấn đề và giải pháp
- Include và ThenInclude - Eager loading
- AsNoTracking - Read-only queries
Giao dịch & Đồng thời
- Transactions - Quản lý transactions
- Concurrency - Xử lý xung đột
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
| State | Description |
|---|---|
Detached | Không được track |
Added | Mới, chưa có trong database |
Unchanged | Khô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
| Method | Tracking | Identity Resolution | Performance |
|---|---|---|---|
AsNoTracking() | ❌ No | ❌ No | Fastest |
AsTracking() | ✅ Yes | ✅ Yes | Default |
AsNoTrackingWithIdentityResolution() | ❌ No | ✅ Yes | Fast |
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:
- Hiểu và áp dụng các nguyên tắc SOLID trong thiết kế class
- Lựa chọn và triển khai design patterns phù hợp
- Thiết kế kiến trúc ứng dụng theo Clean Architecture, DDD, CQRS
- Áp dụng TDD trong phát triển phần mềm
- Lựa chọn kiến trúc hệ thống phù hợp với yêu cầu dự án
- 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
| Principle | Description |
|---|---|
| SRP | Một class, một trách nhiệm |
| OCP | Open for extension, closed for modification |
| LSP | Thay thế được subclass |
| ISP | Nhiều interface nhỏ, không interface lớn |
| DIP | Depend 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
- 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.
- 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?
- SRP: Khi một class đang làm quá nhiều việc, khó test, khó thay đổi
- OCP: Khi cần thêm tính năng mới mà không muốn sửa code hiện có
- LSP: Khi thiết kế inheritance hierarchy, đặc biệt với abstract class
- ISP: Khi interface có quá nhiều methods không liên quan
- 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
- God Class: Class quá lớn, làm nhiều việc (vi phạm SRP)
- Switch Statements: Nhiều switch/case xử lý các type khác nhau (vi phạm OCP)
- Empty Method Overrides: Override method để throw exception (vi phạm LSP)
- Interface Pollution: Interface với nhiều methods không liên quan (vi phạm ISP)
- Concrete Dependencies: Direct instantiation của concrete classes (vi phạm DIP)
Tools hỗ trợ
- SonarQube: Phát hiện code smells vi phạm SOLID
- ReSharper: Gợi ý refactor để tuân thủ SOLID
- Visual Studio Code Metrics: Đo lường maintainability index
- 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-Pattern | Description |
|---|---|
| Premature Abstraction | Creating abstractions before needed |
| Speculative Generality | Building for “future” features |
| Golden Hammer | Using one solution for everything |
| Not Invented Here | Avoiding 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
| Principle | Focus | Action |
|---|---|---|
| DRY | Duplication | Extract shared code |
| KISS | Complexity | Simplify solution |
| YAGNI | Future | Build 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
| Benefit | Description |
|---|---|
| Maintainability | Change one concern without affecting others |
| Testability | Test each concern in isolation |
| Reusability | Reuse concerns across applications |
| Parallel Development | Different teams work on different concerns |
| Flexibility | Easy 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
- Identify Clear Boundaries: Define what each concern is responsible for
- Use Interfaces: Depend on abstractions, not concrete implementations
- Minimize Dependencies: Only depend on concerns you need
- Cohesion: Keep related code together
- Consistency: Apply same separation across entire codebase
Related Concepts
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
| Pattern | Purpose | Complexity |
|---|---|---|
| Singleton | Single instance | Low |
| Factory Method | Subclass decides | Medium |
| Abstract Factory | Families of objects | High |
| Builder | Complex construction | Medium |
| Prototype | Cloning objects | Low-Medium |
Best Practices
- Chọn đúng pattern: Cân nhắc requirements trước khi áp dụng
- Prefer Composition over Inheritance: Builder và Factory thường tốt hơn inheritance
- Consider DI Containers: Trong modern .NET, DI thường replace Singleton pattern
- Immutability: Sử dụng Builder cho immutable objects
- 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
| Pattern | Purpose | Use Case |
|---|---|---|
| Adapter | Convert interface | Legacy integration |
| Bridge | Separate abstraction/implementation | Cross-platform |
| Composite | Tree structure | UI, file systems |
| Decorator | Add behavior dynamically | Feature extension |
| Facade | Simplify interface | Complex subsystems |
| Flyweight | Share objects | Memory optimization |
| Proxy | Control access | Lazy loading, security |
Best Practices
- Adapter: Use when integrating incompatible interfaces
- Bridge: Use when you have multiple platforms/formats
- Composite: Use when representing hierarchical structures
- Decorator: Use when you need flexible behavior extension
- Facade: Use to simplify complex systems
- Flyweight: Use when you have many similar objects
- 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
| Pattern | Purpose | Complexity |
|---|---|---|
| Observer | Event notifications | Low |
| Strategy | Interchangeable algorithms | Low |
| Command | Encapsulated requests | Medium |
| State | Object state behavior | Medium |
| Template Method | Algorithm skeleton | Low |
| Chain of Responsibility | Sequential handlers | Medium |
| Iterator | Collection traversal | Low |
| Mediator | Object communication | Medium |
| Memento | State restoration | Low |
| Visitor | Operations on structures | Medium |
Best Practices
- Observer: Use for loose coupling, event-driven systems
- Strategy: Use when you need multiple algorithms
- Command: Use for undo, queuing, transactions
- State: Use for state machines
- Template Method: Use for algorithm frameworks
- Chain: Use for processing pipelines
- Iterator: Use for custom traversal
- Mediator: Use for reducing dependencies
- Memento: Use for state snapshots
- 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
| Benefit | Description |
|---|---|
| Testability | Easy to unit test business logic |
| Maintainability | Clear structure, easy to navigate |
| Independence | Not tied to frameworks or databases |
| Business Focus | Domain logic is central and clear |
| Flexibility | Easy to change UI or infrastructure |
Comparison with Other Patterns
| Pattern | Focus | Complexity |
|---|---|---|
| Clean Architecture | Business logic independence | High |
| Hexagonal Architecture | Port/Adapter separation | Medium |
| Onion Architecture | Layered dependencies | Medium |
| Layered Architecture | Traditional N-tier | Low |
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
| Benefit | Description |
|---|---|
| Business Focus | Code mirrors business language |
| Complex Domains | Better handling of complex business logic |
| Communication | Improved team communication |
| Testability | Domain logic is easily testable |
| Flexibility | Easier 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
- Clean Architecture
- CQRS
- Event Sourcing
- Eric Evans - “Domain-Driven Design: Tackling Complexity in the Heart of Software”
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
| Aspect | Command | Query |
|---|---|---|
| Purpose | Modify state | Read data |
| Return | void/Result | Data |
| Side Effects | Yes | No |
| idempotent | Usually not | Yes |
// 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
| Benefit | Description |
|---|---|
| Independent Scaling | Scale reads and writes separately |
| Performance | Optimized query models for reads |
| Flexibility | Different data stores for different purposes |
| Security | Easier to secure write operations |
| Complexity | Complex domain logic isolated in commands |
Challenges
| Challenge | Description |
|---|---|
| Complexity | More moving parts than simple CRUD |
| Consistency | eventual consistency between read/write |
| Learning Curve | Team needs to understand pattern |
| Overhead | May 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
| Aspect | CRUD | CQRS |
|---|---|---|
| Model | Single model | Separate models |
| Complexity | Low | Medium-High |
| Scalability | Limited | High |
| Consistency | Immediate | Eventual |
| Best for | Simple apps | Complex 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
- Event Producer: Tạo và publish events
- Event Consumer: Lắng nghe và xử lý events
- Event Channel/Broker: Truyền events từ producers đến consumers
- 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
| Benefit | Description |
|---|---|
| Loose Coupling | Components don’t know about each other |
| Scalability | Scale consumers independently |
| Flexibility | Easy to add new consumers |
| Resilience | Failed events can be retried |
| Audit Trail | Events provide natural audit trail |
| Async Processing | Non-blocking operations |
Challenges
| Challenge | Description |
|---|---|
| Complexity | Harder to debug và trace |
| Eventual Consistency | Data may not be immediately consistent |
| Duplication | Same event processed by multiple consumers |
| Ordering | Events may arrive out of order |
| Testing | Harder to test distributed systems |
Use Cases
| Use Case | Description |
|---|---|
| Microservices | Service-to-service communication |
| Real-time Processing | Stream processing |
| Notifications | Push notifications |
| Audit | Audit logging |
| Workflow | Multi-step processes |
| IoT | Device event processing |
Best Practices
- Idempotency: Design consumers to handle duplicate events
- Event Size: Keep events small, reference data by ID
- Versioning: Plan for event schema changes
- Error Handling: Use retry policies và dead letter queues
- 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
| Benefit | Description |
|---|---|
| Testability | Easy to mock adapters for testing |
| Flexibility | Swap adapters without changing core |
| Maintainability | Clear separation of concerns |
| Framework Independence | Core is not tied to any framework |
| Team Scalability | Different 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
| Aspect | Hexagonal | Clean Architecture |
|---|---|---|
| Focus | Ports & Adapters | Layer dependencies |
| Structure | Hexagonal layers | Circular layers |
| Domain | Pure domain | Domain + Application |
| Complexity | Medium | High |
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
| Benefit | Description |
|---|---|
| Strong Domain Focus | Business logic is central |
| Testability | Easy to test each layer |
| Flexibility | Easy to change infrastructure |
| Maintainability | Clear structure |
| Framework Agnostic | Domain not tied to frameworks |
Comparison with Other Patterns
| Aspect | Onion | Clean | Hexagonal |
|---|---|---|---|
| Layers | 4 layers | 4+ layers | Ports/Adapters |
| Focus | Inward dependencies | Layer separation | Port/Adapter |
| Domain | Entities, Services | Domain + Use Cases | Domain only |
| Infrastructure | Explicit layer | External | Adapters |
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
- Simplified: Fewer explicit layers
- Domain-Centric: Stronger focus on domain
- Flexible: More freedom in implementation
- 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
- Eliminate waste
- Amplify learning
- Decide as late as possible
- Deliver as fast as possible
- Build quality in
- 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
| Method | Flexibility | Documentation | Speed | Team Size |
|---|---|---|---|---|
| Waterfall | Low | High | Low | Any |
| Scrum | High | Low | High | 5-9 |
| Kanban | High | Medium | High | Any |
| XP | High | Medium | High | Small |
| DevOps | High | Low | Very High | Any |
Choosing Right Method
| Project Type | Recommended Method |
|---|---|
| Fixed requirements, safety-critical | Waterfall, V-Model |
| Complex, changing requirements | Scrum, Kanban |
| Small team, fast iteration | XP, Scrum |
| Operations-focused | DevOps |
| Startup, MVP | Agile, 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
- First Law: Không viết production code cho đến khi có một failing unit test
- Second Law: Không viết thêm test gì ngoài cái test đang fail để make it pass
- 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
| Benefit | Description |
|---|---|
| Better Design | Tests force good architecture |
| Fewer Bugs | Catch issues early |
| Living Documentation | Tests document the code |
| Confidence | Refactor without fear |
| Faster Debugging | Know exactly what’s broken |
Challenges
| Challenge | Description |
|---|---|
| Learning Curve | Takes time to get comfortable |
| Over-testing | Don’t test trivial code |
| Test Maintenance | Tests need to evolve with code |
| Slow Start | Seems slower at first |
Best Practices
- Test one thing: Each test should verify one behavior
- Use descriptive names: Test names explain behavior
- Follow AAA: Arrange, Act, Assert
- Keep tests independent: No test should depend on another
- Test edge cases: Include boundary conditions
- 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
| Benefit | Description |
|---|---|
| Clear Communication | Business language in specs |
| Living Documentation | Tests are always up-to-date |
| Shared Understanding | Common language for all team members |
| Focus on Value | Tests describe business value |
| Early Detection | Catch issues early |
Best Practices
- One Scenario per Behavior: Each test should cover one behavior
- Use Meaningful Names: Scenario names should describe the behavior
- Keep Steps Simple: Reuse steps across scenarios
- Automate Everything: Run BDD tests in CI/CD
- Review Together: Team review of feature files
Tools Comparison
| Tool | Language | Use Case |
|---|---|---|
| SpecFlow | C# | .NET applications |
| Cucumber | Java/Ruby | Multi-language |
| Behave | Python | Python applications |
| Jasmine | JavaScript | JavaScript 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
| Factor | Recommended Architecture |
|---|---|
| Small team, simple app | Monolithic |
| Large team, complex domain | Microservices |
| Real-time processing | Event-Driven |
| High read/write ratio | CQRS |
| Frequent changes | Modular Monolith |
References
Microservices
Nguyên tắc Microservice
Đặc điểm chính
| Characteristic | Description |
|---|---|
| Single Responsibility | Mỗi service chỉ làm một việc |
| Loose Coupling | Services giao tiếp qua APIs |
| Independent Deploy | Deploy không ảnh hưởng services khác |
| Technology Diversity | Mỗi service có thể dùng công nghệ khác |
| Ownership | Team 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
| Aspect | RabbitMQ | Kafka |
|---|---|---|
| Protocol | AMQP | Binary (custom) |
| Use Case | Task queues, simple messaging | Event streaming, high throughput |
| Message Retention | Per-queue (short-term) | Topic-based (long-term) |
| Ordering | Per-queue | Per-partition |
| Scalability | Horizontal | Very high |
| Complexity | Simpler | More complex |
| Delivery Guarantee | At-least-once, Exactly-once | At-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
| Advantage | Description |
|---|---|
| Simplicity | Easy to develop và debug |
| Performance | In-process calls are fast |
| Consistency | Single codebase, shared resources |
| Transaction | Easy to maintain ACID transactions |
| Deployment | Simple deployment (single package) |
| Development | Good for small teams |
Disadvantages
| Disadvantage | Description |
|---|---|
| Scalability | Khó scale theo component |
| Technology | Hard to adopt new technologies |
| Reliability | One failure affects entire app |
| Deployment | Phải redeploy toàn bộ app |
| Development | Codebase grows large, hard to maintain |
| Coupling | Tightly 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
- Use Layers: Separate concerns within the monolith
- Modularize: Group code by feature, not by technical layer
- Configuration: Externalize configuration
- Logging: Implement centralized logging
- 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
| Benefit | Description |
|---|---|
| No Server Management | No OS, patching, or capacity planning |
| Auto-scaling | Automatic scaling from 0 to thousands |
| Pay per use | Pay only for compute time used |
| Faster time to market | Focus on business logic |
| Reduced operational overhead | Cloud provider handles infrastructure |
| Built-in high availability | Multiple AZ redundancy |
Challenges
| Challenge | Description |
|---|---|
| Cold starts | Initial invocation latency |
| Vendor lock-in | Platform-specific APIs |
| Testing complexity | Hard to test locally |
| Statelessness | Must handle state externally |
| Debugging | Distributed tracing complexity |
| Security | Need 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 Case | Description |
|---|---|
| Web backends | APIs và web applications |
| Data processing | ETL, batch processing |
| Real-time file processing | Image resizing, video transcoding |
| IoT backends | IoT data ingestion |
| Chatbots | NLP và message handling |
| Scheduled tasks | Cron jobs, cleanup tasks |
| Event-driven apps | Event handling, notifications |
Comparison
| Aspect | Serverless | Traditional |
|---|---|---|
| Server management | Provider | Self |
| Scaling | Automatic | Manual/auto |
| Pricing | Per execution | Always on |
| Deployment | Functions | Containers/servers |
| Latency | Cold starts | Always ready |
| Complexity | Distributed | Monolithic |
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
| Benefit | Description |
|---|---|
| Complete Audit | Full history of all changes |
| Temporal Queries | Query state at any point in time |
| Event Replay | Recreate state by replaying events |
| Debugging | Replay events to understand bugs |
| Scalability | Append-only event store |
| Flexibility | Easy to add new projections |
Challenges
| Challenge | Description |
|---|---|
| Complexity | More complex than traditional CRUD |
| Learning Curve | Harder to understand |
| Event Schema | Changes require migration strategies |
| Storage | Can grow large (need snapshots) |
| Consistency | Eventual consistency |
Use Cases
| Use Case | Description |
|---|---|
| Banking | Complete transaction history |
| Audit | Full audit trail requirements |
| Collaboration | Activity feeds |
| E-commerce | Order processing |
| Gaming | Game state management |
| IoT | Event logging |
Comparison
| Aspect | Traditional | Event Sourcing |
|---|---|---|
| Storage | Current state | History of changes |
| Audit | Add audit tables | Built-in |
| Queries | Query current state | Project from events |
| Complexity | Lower | Higher |
| Debugging | Harder | Replay 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
- In-Memory Cache - IMemoryCache
- Distributed Cache - Redis
- Response Caching - Response caching headers
Xử lý tải
- Rate Limiting - Giới hạn requests
- Load Balancing - Phân phối tải
- Health Checks - Kiểm tra health
Bất đồng bộ
- IAsyncEnumerable - Stream dữ liệu
- Kỹ thuật stream - Large data handling
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
}
| Aspect | Task | ValueTask |
|---|---|---|
| Allocation | Always heap | Avoided if sync |
| Use case | Standard async | Hot path |
| Synchronous return | Not allowed | Allowed |
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
- RabbitMQ - Message broker với queue model
- Kafka - Distributed event streaming
- Pub/Sub Pattern - Publish/Subscribe model
Container & Cloud
- Docker - Containerization
- Kubernetes - Orchestration
- Azure/AWS - Cloud deployment
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
- Azure Queue Storage - Basic implementation, sending/receiving messages
3. Best Practices
- Queue Best Practices - Error handling, retry patterns, dead letter queues, idempotency
4. Multiple Queues
- Multiple Queues - Managing multiple queue clients, factory pattern
5. Azure Service Bus
- Service Bus Topics - Pub/Sub patterns, filters, subscriptions
Quick Comparison
| Service | Type | Max Message | Best For |
|---|---|---|---|
| Azure Queue Storage | Point-to-Point | 64 KB | Simple, cost-effective |
| Azure Service Bus | Pub/Sub | 256 KB | Enterprise features |
| RabbitMQ | Hybrid | 64 MB | Custom deployment |
| Kafka | Log-based | Unlimited | High 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 Case | Recommended Service |
|---|---|
| Order Processing | Azure Queue |
| Background Jobs | Azure Queue |
| Email Notifications | Service Bus Topics |
| Event Distribution | Service Bus Topics |
| Microservices | Service 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
- Start with Introduction to understand the basics
- Move to Azure Queue Storage for implementation
- Learn Best Practices for production systems
- Explore Service Bus Topics for Pub/Sub
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
| Benefit | Description |
|---|---|
| Decoupling | Producers và consumers không cần biết về nhau |
| Reliability | Messages được lưu persistently cho đến khi processed |
| Scalability | Thêm consumers không ảnh hưởng producers |
| Load Leveling | Xử lý traffic spikes bằng cách queue messages |
| Asynchronous | Non-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 Case | Description |
|---|---|
| Order Processing | Xử lý đơn hàng asynchronously |
| Background Jobs | Scheduled tasks, batch processing |
| Microservices Communication | Service-to-service messaging |
| Email/Notifications | Async notification delivery |
| File Processing | Upload, transform, process |
| Event-Driven Architecture | System-wide event distribution |
Common Message Queue Services
| Service | Provider | Type | Best For |
|---|---|---|---|
| Azure Queue Storage | Microsoft | Point-to-Point | Simple, cost-effective |
| Azure Service Bus | Microsoft | Pub/Sub | Enterprise features |
| RabbitMQ | Open Source | Hybrid | Flexible deployment |
| Kafka | Open Source | Log-based | High throughput |
| AWS SQS | Amazon | Point-to-Point | AWS integration |
| AWS SNS | Amazon | Pub/Sub | AWS integration |
Next Steps
- Azure Queue Storage - Basic implementation
- Queue Best Practices - Error handling, retry
- Multiple Queues - Managing multiple queues
- Service Bus Topics - Pub/Sub patterns
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
| Feature | Limit |
|---|---|
| Max message size | 64 KB |
| Max queue size | 500 TB |
| Max messages per retrieval | 32 |
| Default visibility timeout | 30 seconds |
| Max visibility timeout | 7 days |
| Max TTL | 7 days |
Next Steps
- Queue Best Practices - Error handling, retry
- Multiple Queues - Managing multiple queues
- Introduction - Back to overview
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
| Practice | Description |
|---|---|
| Exponential Backoff | Increase delay between retries |
| Dead Letter Queue | Store failed messages for investigation |
| Poison Message Handling | Detect and archive repeatedly failing messages |
| Circuit Breaker | Stop processing when failures are too frequent |
| Idempotency | Ensure message can be processed multiple times safely |
| Monitoring | Track queue health and alert on issues |
Next Steps
- Multiple Queues - Managing multiple queues
- Azure Service Bus Topics - Pub/Sub patterns
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);
}
}
2. Queue Client Factory (Recommended for DI)
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
| Approach | Best For | Pros | Cons |
|---|---|---|---|
| Multiple instances | Small apps | Simple, direct | Repetitive code |
| Factory | Medium apps | DI-friendly, reusable | More setup |
| Generic service | Large apps | Centralized, flexible | Slightly complex |
| Background service | Processing | Handles multiple queues | Single process |
| Config-based | Config-driven | Easy to change | Less type-safe |
| Queue-specific processors | Complex apps | Separation of concerns | More interfaces |
Next Steps
- Azure Service Bus Topics - Pub/Sub patterns
- Introduction - Back to overview
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
| Feature | Azure Queue | Service Bus Topics |
|---|---|---|
| Pattern | Point-to-Point | Pub/Sub |
| Multiple Consumers | One consumer per message | Multiple consumers |
| Message Size | 64 KB | 256 KB |
| Sessions | Not supported | Supported |
| Transactions | Not supported | Supported |
| Duplicate Detection | Not supported | Supported |
| Ordering | FIFO | FIFO per subscription |
When to Use Topics
| Scenario | Solution |
|---|---|
| Multiple systems need same data | Pub/Sub |
| Different processing per message type | Filters |
| Fan-out to microservices | Multiple subscriptions |
| Event-driven architecture | Topics + Subscriptions |
Summary
| Component | Description |
|---|---|
| Topic | Channel for publishing messages |
| Subscription | Recipient that receives messages from topic |
| Filter | Rules to route messages to subscriptions |
| Action | Modify 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
| Factor | Description |
|---|---|
| Codebase | One codebase tracked in version control |
| Dependencies | Explicitly declare dependencies |
| Config | Store config in environment |
| Backing Services | Treat backing services as attached resources |
| Build/Release/Run | Strictly separate build and run stages |
| Processes | Execute app as one or more stateless processes |
| Port Binding | Export HTTP as a service by port binding |
| Concurrency | Scale out via process model |
| Disposability | Fast startup and graceful shutdown |
| Dev/Prod Parity | Keep development, staging, production similar |
| Logs | Treat logs as event streams |
| Admin Processes | Run 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 với xUnit - Viết unit tests
- Mocking với Moq - Mock dependencies
- Integration Testing - Test toàn bộ pipeline
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 |
|---|---|---|
| Platform | Cross-platform (Windows, Linux, macOS) | Windows only |
| Source | Open source | Mostly closed source |
| Performance | Better, optimized runtime | Legacy |
| Modularity | Package-based (NuGet) | Full framework |
| CLI | Full CLI support | Limited |
| Side-by-side | Multiple versions | Single version per machine |
| Use case | Modern apps, microservices | Legacy 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
| Feature | MVC | Web API | Minimal API |
|---|---|---|---|
| Views | ✅ Yes | ❌ No | ❌ No |
| Controllers | ✅ Yes | ✅ Yes | ❌ No |
| Routing | Convention + Attribute | Attribute | MapGet/MapPost |
| Model Binding | Full | Full | Limited |
| Testability | Medium | Medium | High |
| Use case | Web apps | APIs | Microservices |
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
| Aspect | Abstract Class | Interface |
|---|---|---|
| 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 |
| Inheritance | Single | Multiple |
| 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
| Aspect | IEnumerable | IQueryable |
|---|---|---|
| Location | Client-side | Server-side |
| Execution | In-memory | Database query |
| SQL Translation | ❌ No | ✅ Yes |
| Deferred | Yes | Yes |
| Use case | In-memory collections | Database queries |
| Performance | Slow with large data | Optimized |
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
| Aspect | var | dynamic |
|---|---|---|
| Type checking | Compile-time | Runtime |
| IntelliSense | ✅ Yes | ❌ No |
| Performance | Fast | Slower |
| Use case | Type known at compile | Late binding |
const vs readonly
| Aspect | const | readonly |
|---|---|---|
| Evaluation | Compile-time | Runtime |
| Value | Must be known at compile | Set at runtime |
| Static | Always static | Can be instance-level |
| Use case | Compile-time constants | Runtime constants |
string vs StringBuilder
| Aspect | string | StringBuilder |
|---|---|---|
| Type | Immutable | Mutable |
| Memory | New allocation per change | Dynamic buffer |
| Use case | Small strings, no changes | Large 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ức | Mô tả |
|---|---|---|
| 1 | JSX & Rendering | JSX syntax, Virtual DOM, Conditional rendering, List rendering |
| 2 | Components & Props | Functional components, Class components, Props, Children |
| 3 | Hooks Cơ bản | useState, useEffect, useRef, useId |
| 4 | Hooks Nâng cao | useReducer, useMemo, useCallback, useLayoutEffect, Custom Hooks |
| 5 | Context API | createContext, useContext, Provider pattern |
| 6 | Redux & Redux Toolkit | Store, Slice, Thunk, RTK Query |
| 7 | React Query | Data fetching, Caching, Mutations, Pagination |
| 8 | React Router | Routes, Navigation, Nested routes, Protected routes |
| 9 | Forms & Validation | Controlled forms, React Hook Form, Zod validation |
| 10 | Styling | CSS Modules, styled-components, Tailwind CSS |
| 11 | Performance | Memo, Code splitting, Lazy loading, Profiler |
| 12 | Testing | Jest, React Testing Library, Mock API |
| 13 | React Patterns | HOC, Render Props, Compound Components, Portals |
| 14 | Next.js Cơ bản | SSR, SSG, ISR, App Router, Server Components |
Hướng dẫn sử dụng
- Bắt đầu từ nền tảng: JSX → Components → Hooks
- State Management: Context API cho app nhỏ, Redux/Redux Toolkit cho app lớn
- Data Fetching: React Query là tiêu chuẩn hiện đại
- 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ăng | Class Component | Functional Component |
|---|---|---|
| Syntax | Verbose | Ngắn gọn |
| State | this.state | useState |
| Lifecycle | Methods cụ thể | useEffect |
this | Cần bind | Không có |
| Tái sử dụng logic | HOC, Render Props | Custom Hooks |
| Performance | Tương đương | Tươ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
useState | useRef | |
|---|---|---|
| Gây re-render | ✅ Có | ❌ Không |
| Lưu qua renders | ✅ Có | ✅ Có |
| Tương tự | State | Instance variable |
| Dùng khi | Cần UI cập nhật | Không cần UI cập nhật |
| Ví dụ | Form values | Timer 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 API | Redux Toolkit |
|---|---|---|
| Độ phức tạp | Đơn giản | Phức tạp hơn |
| App size | Nhỏ-vừa | Lớn |
| DevTools | Không có | ✅ Có |
| Time-travel debug | Không | ✅ Có |
| Performance | Cần tối ưu thủ công | Được tối ưu |
| Middleware | Không | ✅ Có (Thunk, Saga) |
| Ví dụ phù hợp | Theme, Auth, Language | Shopping 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>
);
}
useNavigate vs Link
Link / NavLink | useNavigate | |
|---|---|---|
| Dùng khi | Navigation trong JSX | Navigation trong event handlers |
| Accessibility | ✅ Semantic <a> tag | ❌ Cần thêm manually |
| Ví dụ | Menu items, buttons | Form 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 Modules | Tailwind CSS | styled-components | |
|---|---|---|---|
| Bundle size | Nhỏ | Nhỏ (purge) | Lớn hơn |
| DX | Tốt | Rất tốt | Tốt |
| Type-safe | Hạn chế | ❌ | ✅ (với TS) |
| Theming | Thủ công | Config file | Theme object |
| Runtime | ❌ | ❌ | ✅ CSS-in-JS |
| Learning curve | Thấp | Trung bình | Trung bình |
| Phù hợp | Mọi dự án | Dự án mới | Component 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
| Strategy | Khi nào | Ví dụ |
|---|---|---|
| SSG (Static) | Content ít thay đổi | Blog, Landing page |
| ISR | Content thay đổi vừa | Product page, News |
| SSR | Data real-time, user-specific | Dashboard, Checkout |
| CSR | Interactive, no SEO needed | Admin panel widgets |
| Streaming | Nhiều data sources | Complex 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ả |
|---|---|---|
| 1 | Concepts Cơ bản & Kết nối | Index, Shard, Replica, DI setup trong ASP.NET Core |
| 2 | Mapping & Field Types | Attribute mapping, Fluent mapping, text vs keyword, nested |
| 3 | Indexing Documents | CRUD, Bulk API, Upsert, Ingest Pipeline qua .NET |
| 4 | Basic Search | Match, Term, Range, Bool query, Sorting qua .NET |
| 5 | Query DSL Nâng cao | Multi-match, Fuzzy, Nested, Highlight, Scroll |
| 6 | Aggregations | Metric, Terms, Range, Date Histogram, Faceted Search |
| 7 | Analyzers & Tokenizers | Built-in analyzers, Custom analyzer, Autocomplete |
| 8 | Performance Tuning | Filter vs must, Source filtering, Search After, ILM |
| 9 | Cluster Management | Health check, ILM, Alias, Snapshot, ASP.NET Core integration |
Use Cases
| Use Case | Phù 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 trongProgram.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;
Match Query - Full-text Search
// 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; } = [];
}
Kết hợp Search + Aggregations (Faceted Search)
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 và 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})");
}
| Analyzer | Mô tả | Ví dụ output |
|---|---|---|
standard | Mặc định - tách theo Unicode, lowercase | “Hello World” → [“hello”, “world”] |
simple | Tách theo ký tự không phải chữ | “IP 192.168.1.1” → [“ip”] |
whitespace | Tách theo khoảng trắng | “foo bar” → [“foo”, “bar”] |
english | Stemming tiếng Anh + stop words | “running foxes” → [“run”, “fox”] |
keyword | Khô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
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
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
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
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
- Số liệu cụ thể: Luôn có metrics để chứng minh impact
- Tập trung vào backend: Deep dive vào technical decisions
- Thể hiện learning mindset: Bài học rút ra và cải tiến
- 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
-
- Google Maps Integration
- Optimizely CMS Architecture
- Backend API Design
- Caching Strategies
-
- Real-time Monitoring System
- ElasticSearch Integration
- Webhook Engine Design
- WPF & MVVM Architecture
-
- Background Job Architecture
- Message Queue Implementation
- Report Generation Optimization
- Pre-calculation Strategy
-
- 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
-
Location Management
- CRUD operations cho locations trong CMS
- Import/export bulk locations từ CSV/Excel
- Validation địa chỉ và tọa độ
-
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
-
Frontend Display
- Store locator page cho end users
- Search & filter locations (by radius, category)
- Direction integration (Google Directions API)
- Responsive design cho mobile
-
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:
- Server-side clustering: Group nearby locations trên server
- Viewport-based loading: Chỉ load markers trong viewport
- 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
| Metric | Before | After | Improvement |
|---|---|---|---|
| Page load time | 4.5s | 1.8s | 60% faster |
| Map render time | 2.1s | 0.4s | 81% faster |
| API response time (p95) | 850ms | 120ms | 86% faster |
| Cache hit rate | - | 94% | - |
| Import processing time | 15 min | 3 min | 80% 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
- Caching is critical: Luôn cache geospatial queries và map tiles
- Lazy loading matters: Không load tất cả markers cùng lúc
- Background jobs: Offload heavy processing khỏi request pipeline
- Rate limiting: Protect external API calls với rate limiters
Soft Skills
- Communication: Regular sync với stakeholders để understand requirements
- Documentation: Viết docs cho APIs và CMS integration
- 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:
- Spatial indexes: Tạo index trên columns (Latitude, Longitude)
- Bounding box first: Filter nhanh với bounding box trước khi tính distance
- Covering indexes: Include các columns thường select
- 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
-
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)
-
Alert & Notification
- Configurable thresholds cho từng sensor type
- Multi-channel notifications (Email, SMS, Webhook)
- Alert escalation rules
-
Webhook Engine
- User-defined rules với custom conditions
- Trigger external APIs khi có events
- Retry mechanism cho failed webhooks
-
Reporting & Analytics
- Daily/Weekly/Monthly reports
- OEE (Overall Equipment Effectiveness) calculations
- Downtime analysis
-
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
| Metric | Before | After | Improvement |
|---|---|---|---|
| Event processing latency | 5.2s | 0.8s | 85% faster |
| Chart render time (10k points) | 3.5s | 0.3s | 91% faster |
| Webhook success rate | 78% | 99.5% | 27% improvement |
| Data retention | 30 days | 2 years | 24x longer |
| Alert delivery time | 15s | 2s | 87% 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
- Time-series data cần special handling: Index rollover, downsampling, aggregation
- Circuit breaker là must-have: Khi tích hợp với external systems
- Batch processing > Real-time cho high volume: Trade-off giữa latency và throughput
- Monitoring & Alerting: Cần instrument mọi thứ từ day 1
Soft Skills
- Domain knowledge quan trọng: Hiểu factory operations để design đúng
- Stakeholder management: Operators vs Managers có different needs
- 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:
- Sharding strategy: Partition theo device_id range hoặc geo-region
- Edge computing: Pre-process data tại edge trước khi gửi về central
- Kafka thay vì Service Bus: Cho higher throughput
- Data tiering: Hot data in memory (Redis), warm in ES, cold in data lake
- 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:
- Report generation: Mất 15-20 giây để tạo báo cáo
- Calculation performance: Tính toán pension payout mất 5-10 giây cho mỗi member
- Database load: Heavy queries làm chậm hệ thống trong giờ cao điểm
- User experience: Users phải wait cho synchronous processing
Yêu cầu
Functional Requirements
-
Member Management
- CRUD operations cho members
- Employment history tracking
- Salary history và contribution tracking
-
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
-
Reporting
- Monthly/Quarterly/Annual reports
- Regulatory reports (government compliance)
- Ad-hoc reports với custom filters
- Export (PDF, Excel, CSV)
-
Payment Processing
- Monthly payment runs
- Direct deposit integration
- Payment adjustments
- Arrears calculation
-
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
| Metric | Before | After | Improvement |
|---|---|---|---|
| Report generation time | 15-20s | 2-3s | 85% faster |
| Pension calculation time | 5-10s | < 1s (cached) | 90% faster |
| Database CPU (peak) | 95% | 45% | 53% reduction |
| API response time (p95) | 2.5s | 0.3s | 88% faster |
| Background job success rate | 82% | 99.8% | 22% improvement |
| User satisfaction | 3.2/5 | 4.6/5 | 44% 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
- Pre-calculation is key: Don’t calculate on-the-fly cho complex business logic
- Event-driven architecture: Decouple components với message queues
- Batch processing: Group operations để giảm database round trips
- Caching strategy: Multi-level caching (memory, distributed, database)
- Observability: Instrument mọi thứ từ đầu (metrics, logs, traces)
Architecture Learnings
- Transactional Outbox: Đảm bảo event consistency
- Saga Pattern: Handle distributed transactions
- CQRS: Separate read/write models cho optimization
- Versioning: Rule versioning cho regulatory changes
Soft Skills
- Working in large team: Clear APIs, documentation, code reviews
- Stakeholder communication: Translate technical to business value
- 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:
- Unit tests: Test từng calculation component với known inputs/outputs
- Integration tests: End-to-end tests với test data sets
- Parallel run: Run new system alongside old system, compare results
- Audit trail: Lưu full calculation history với input snapshots
- Reconciliation: Daily reconciliation reports
- 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:
| Aspect | Pre-calculation | On-the-fly |
|---|---|---|
| Read latency | Very fast (cache hit) | Slow |
| Write latency | Higher (trigger calc) | Lower |
| Data freshness | Eventually consistent | Always current |
| Storage | Higher (store results) | Lower |
| Complexity | Higher (job scheduling) | Lower |
| Best for | Read-heavy, complex calc | Write-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:
- Event Sourcing: Store events instead of current state → full audit trail, temporal queries
- GraphQL API: More flexible queries cho frontend
- Microservices: Split by bounded contexts (Member, Calculation, Payment)
- Data Lake: Store historical data cho analytics và ML
- Better testing: More property-based tests cho calculation logic
- 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
- Technical Questions by Project
- System Design Questions
- Backend Deep Dive
- Behavioral Questions
- 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;
}
}
- 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**:
-
Requirements:
- Schedule and process jobs
- Retry failed jobs
- Priority queues
- Progress tracking
-
Architecture: [API] → [Job Queue] → [Workers] ↓ ↓ [Status DB] [Job Store]
-
Key Components:
- Queue: Azure Service Bus / RabbitMQ
- Scheduler: Hangfire / Quartz.NET
- Storage: SQL Server cho persistence
- Monitoring: Dashboard + alerts
-
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**:
-
Retry Policy:
- Immediate retry: 3 attempts với delay
- Exponential backoff: 1s, 10s, 60s
- Max retries: 3-5 tùy criticality
-
Dead Letter Queue:
- Move failed messages sau max retries
- Manual inspection và reprocessing
- Alert on DLQ size threshold
-
Idempotency:
- Unique message ID
- Check processed messages before handling
- Deduplication window (24 hours)
-
Monitoring:
- Track failure rate per queue
- Alert on sudden spikes
- Dashboard với queue depths
---
### Caching Strategies
#### Q: Cache invalidation strategies?
**A**:
-
Time-based (TTL):
- Simple, automatic
- Risk: Stale data within TTL
- Use: Frequently changing data
-
Event-based:
- Invalidate on data change
- More accurate
- Use: Critical data consistency
-
Write-through:
- Update cache and DB together
- Strong consistency
- Use: Read-heavy, write-rarely
-
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**:
-
Indexing:
- Covering indexes (INCLUDE columns)
- Composite indexes (order matters!)
- Filtered indexes for subsets
-
Query Patterns:
- Avoid SELECT *
- Use EXISTS thay vì COUNT for existence checks
- Parameterized queries (plan caching)
-
Execution Plans:
- Look for table scans → add indexes
- Check join types (Nested Loop vs Hash Match)
- Statistics updates
-
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 |
|---|---|---|---|
| 1 | Giải phương trình bậc nhất | Easy | Exception handling, validation, result pattern |
| 2 | Best Time to Buy/Sell Stock | Easy | Single 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:
- Đề bài: Mô tả yêu cầu, input/output
- Phân tích: Requirements, edge cases, constraints
- Giải pháp: Code với giải thích chi tiết
- Unit Tests: Test cases đầy đủ
- Mở rộng: Variants phức tạp hơn của bài toán
- 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
| Aspect | Double Input | Integer Input |
|---|---|---|
| EPSILON | ✅ Cần | ❌ Không |
| Comparison | Math.Abs() > EPSILON | != 0 |
| Validation | NaN, Infinity | Không cần |
| Precision | Floating point errors | Exact |
| Complexity | Phứ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 định | Lý do |
|---|---|
| Static class | Không cần state, tiết kiệm memory |
| Exception cho errors | Global exception handling đã có |
| TrySolve pattern | Cho expected no solution cases |
| EPSILON = 1e-10 | Cân bằng giữa precision và practical |
| Fraction struct | Exact representation cho integer inputs |
| Record struct | Type-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 mua và 1 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
| Version | Time | Space | Khi nào dùng |
|---|---|---|---|
| Single transaction | O(n) | O(1) | Bài toán cơ bản |
| With details | O(n) | O(1) | Cần biết buy/sell days |
| Multiple transactions | O(n) | O(1) | Unlimited buys/sells |
| K transactions | O(kn) | O(kn) | Giới hạn số lần |
| With fee | O(n) | O(1) | Có transaction cost |
Design Decisions
| Quyết định | Lý do |
|---|---|
| Static class | Không cần state, không tốn memory cho object |
| Return int | Direct, dễ dùng |
| ValueTuple | Return multiple values without class overhead |
| Exception cho null | Global handling đã có |
| No enum/class | Simple 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
-
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
-
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
-
Mô hình kiến trúc - Các kiến trúc phổ biến
- Monolithic, Microservices, Event-Driven, Serverless, Layered
-
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
-
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
-
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
-
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/123luô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
- URL Shortener (TinyURL) - Hệ thống rút gọn URL
- Chat Application (WhatsApp/Telegram) - Ứng dụng chat real-time
- Social Media Feed (Twitter) - News feed cho mạng xã hội
- E‑commerce Platform (Amazon) - Sàn thương mại điện tử
- Ride‑sharing (Uber) - Ứng dụng gọi xe
- 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:
- Bước 1: Thu thập yêu cầu - Functional & Non-functional requirements, Scale estimation
- Bước 2: Ước lượng - Traffic, Storage, Bandwidth estimates
- Bước 3: Thiết kế high‑level - Block diagram, Components, Technology selection
- Bước 4: Thiết kế chi tiết - Database schema, API design, Data flow
- Bước 5: Bottlenecks & Tối ưu - SPOF, Scalability, Performance
- 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
| Column | Type | Description |
|---|---|---|
| short_key | VARCHAR(7) | Primary key, unique |
| original_url | TEXT | URL gốc |
| user_id | BIGINT | Owner (optional) |
| created_at | TIMESTAMP | Thời gian tạo |
| click_count | BIGINT | Số 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:
- Client gửi URL dài → API.
- Generate unique ID (counter/hash).
- Encode ID → short_key.
- Lưu vào database + cache.
- Return short_url.
Redirect:
- Client request /abc123.
- Check cache → nếu có, redirect ngay.
- Nếu không có, query database.
- Lưu vào cache.
- 301 Redirect.
- 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
| Approach | Pros | Cons |
|---|---|---|
| Counter | Unique, no collision | Predictable, need distributed counter |
| Hash | Non-predictable | Collision 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
| Column | Type | Description |
|---|---|---|
| message_id | UUID | Primary key |
| chat_id | BIGINT | Chat room ID (indexed) |
| sender_id | BIGINT | User ID |
| content | TEXT | Message content |
| media_url | TEXT | Optional media link |
| created_at | TIMESTAMP | Time (clustering key) |
| status | TINYINT | Sent/Delivered/Read |
Table: chat_members
| Column | Type | Description |
|---|---|---|
| chat_id | BIGINT | Chat ID |
| user_id | BIGINT | User ID |
| role | TINYINT | Admin/Member |
| joined_at | TIMESTAMP | Join time |
Table: user_presence
| Column | Type | Description |
|---|---|---|
| user_id | BIGINT | Primary key (Redis) |
| status | TINYINT | Online/Offline |
| last_seen | TIMESTAMP | Last 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):
- Client gửi message qua WebSocket.
- Chat Service nhận, validate.
- Lưu message vào database (partition by chat_id).
- Publish event đến Kafka topic
chat-{chat_id}. - Delivery Service subscribe, forward đến recipient.
- Update cache (recent messages).
- Send push notification nếu recipient offline.
Receive Message:
- Delivery Service nhận từ Kafka.
- Lookup recipient’s connection (which server).
- Forward qua WebSocket connection.
- Client ack → update status thành “delivered”.
- Khi user đọc → update “read” status.
Presence System:
- Khi client connect → set Redis key
presence:{user_id}= online. - Heartbeat mỗi 30s để giữ connection.
- Khi disconnect/timer expire → set offline + last_seen.
- 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
| Approach | Pros | Cons |
|---|---|---|
| WebSocket | Real-time, low latency, bidirectional | Complex, battery drain |
| Long Polling | Simple, works everywhere | Higher 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
| Column | Type | Description |
|---|---|---|
| tweet_id | BIGINT | Primary key (Snowflake ID) |
| user_id | BIGINT | Author ID (indexed) |
| content | TEXT | Tweet text |
| media_urls | JSON | Optional media links |
| created_at | TIMESTAMP | Time (clustering key) |
| like_count | INT | Denormalized count |
| retweet_count | INT | Denormalized count |
Table: follows
| Column | Type | Description |
|---|---|---|
| follower_id | BIGINT | User who follows |
| followee_id | BIGINT | User being followed |
| created_at | TIMESTAMP | Follow time |
| Primary Key: (follower_id, followee_id) |
Table: user_feed (Redis)
| Key | Type | Description |
|---|---|---|
| feed:{user_id} | Sorted Set | Tweet 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)
3. Hybrid Model (Recommended)
- 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:
- Client POST /tweets.
- Validate, store in Cassandra.
- Lookup followers (from cache).
- If normal user: Push to followers’ feed cache.
- If celebrity: Store only, pull on read.
- Update search index (async via Kafka).
- Return tweet.
Load Feed:
- Client GET /feed.
- Load precomputed feed from Redis (50 tweets).
- Fetch recent tweets from celebrities (pull).
- Merge & sort by timestamp.
- Return feed.
- 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
| Model | Write Cost | Read Cost | Best For |
|---|---|---|---|
| Pull | O(1) | O(N * M) | Celebrities |
| Push | O(N) | O(1) | Normal users |
| Hybrid | O(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
| Column | Type | Description |
|---|---|---|
| product_id | BIGINT | Primary key |
| title | VARCHAR(500) | Product name |
| description | TEXT | Product description |
| price | DECIMAL(10,2) | Current price |
| category_id | BIGINT | Foreign key |
| inventory_count | INT | Stock level |
| created_at | TIMESTAMP | Created time |
Table: users
| Column | Type | Description |
|---|---|---|
| user_id | BIGINT | Primary key |
| VARCHAR(255) | Unique, indexed | |
| password_hash | VARCHAR(255) | Hashed password |
| created_at | TIMESTAMP | Join date |
Table: orders
| Column | Type | Description |
|---|---|---|
| order_id | UUID | Primary key |
| user_id | BIGINT | Foreign key |
| total_amount | DECIMAL(10,2) | Order total |
| status | TINYINT | Pending/Paid/Shipped/Delivered |
| created_at | TIMESTAMP | Order time |
| Index: (user_id, created_at) |
Table: order_items
| Column | Type | Description |
|---|---|---|
| order_id | UUID | Foreign key |
| product_id | BIGINT | Foreign key |
| quantity | INT | Quantity ordered |
| price | DECIMAL(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:
- Client request /products với filters.
- API Gateway → Product Service.
- Query Elasticsearch với filters.
- Return results với pagination.
- Cache popular searches.
Add to Cart:
- Client POST /cart/items.
- Cart Service validate product availability.
- Store in Redis hash:
cart:{user_id}. - Set TTL 30 days.
- Return updated cart.
Checkout:
- Client POST /orders.
- Order Service create order (PENDING).
- Reserve inventory (decrement count).
- Call Payment Service.
- Payment success → update order status (PAID).
- Publish
order.placedevent. - Inventory Service consume event, update stock.
- Shipping Service consume event, prepare shipment.
Payment Processing:
- Payment Service call Stripe/PayPal API.
- Handle 3D Secure nếu cần.
- Store payment result.
- Idempotency key để tránh double charge.
- 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
| Approach | Pros | Cons |
|---|---|---|
| Monolith | Simple, ACID transactions | Hard to scale, deployment risk |
| Microservices | Independent scaling, deployment | Distributed 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/day → 36 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
| Column | Type | Description |
|---|---|---|
| ride_id | UUID | Primary key |
| rider_id | BIGINT | Foreign key |
| driver_id | BIGINT | Foreign key (nullable khi chưa match) |
| pickup_lat | DECIMAL(9,6) | Pickup location |
| pickup_lng | DECIMAL(9,6) | Pickup location |
| dropoff_lat | DECIMAL(9,6) | Dropoff location |
| dropoff_lng | DECIMAL(9,6) | Dropoff location |
| status | TINYINT | Requested/Matched/InProgress/Completed/Cancelled |
| fare | DECIMAL(10,2) | Final fare |
| created_at | TIMESTAMP | Request time |
Table: drivers
| Column | Type | Description |
|---|---|---|
| driver_id | BIGINT | Primary key |
| user_id | BIGINT | Foreign key |
| vehicle_info | JSON | Car model, plate, color |
| rating | DECIMAL(3,2) | Average rating |
| status | TINYINT | Available/Busy/Offline |
Table: locations (Time-series, partitioned)
| Column | Type | Description |
|---|---|---|
| driver_id | BIGINT | Partition key |
| timestamp | TIMESTAMP | Clustering key |
| lat | DECIMAL(9,6) | GPS latitude |
| lng | DECIMAL(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:
- Rider POST /rides/request.
- Ride Service create ride (REQUESTED).
- Calculate fare (base + distance + time + surge).
- Dispatch Service tìm nearby drivers (5km radius).
- Filter available drivers, rank by distance/rating.
- Send ride request to top drivers (via push notification).
- First driver accepts → match.
- Update ride status (MATCHED), notify rider.
Driver Location Update:
- Driver app gửi location mỗi 3s qua WebSocket.
- Location Service receive, validate.
- Update Redis GeoIndex:
drivers:online. - Stream to Kafka topic
driver-locations. - LocationHistory Service consume, store in Cassandra.
- 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
| Column | Type | Description |
|---|---|---|
| video_id | UUID | Primary key |
| uploader_id | BIGINT | Foreign key |
| title | VARCHAR(500) | Video title |
| description | TEXT | Video description |
| duration | INT | Duration in seconds |
| status | TINYINT | Processing/Ready/Private |
| view_count | BIGINT | Denormalized count |
| created_at | TIMESTAMP | Upload time |
Table: video_renditions
| Column | Type | Description |
|---|---|---|
| rendition_id | UUID | Primary key |
| video_id | UUID | Foreign key |
| resolution | VARCHAR(10) | 1080p, 720p, 480p, etc. |
| bitrate | INT | Bitrate in kbps |
| codec | VARCHAR(20) | H.264, H.265, VP9 |
| s3_key | VARCHAR(500) | S3 object key |
| file_size | BIGINT | File size in bytes |
Table: video_segments
| Column | Type | Description |
|---|---|---|
| rendition_id | UUID | Foreign key |
| segment_num | INT | Segment number (0, 1, 2…) |
| s3_key | VARCHAR(500) | Segment file key |
| duration | DECIMAL(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:
- Client POST /videos/upload.
- Create video metadata (status=PROCESSING).
- Return presigned S3 URL.
- Client upload directly to S3.
- S3 event → SQS message.
- Transcoding Service consume message.
- Download, transcode, upload renditions.
- Update video status (READY).
- Invalidate CDN cache.
- Notify user.
Stream Video:
- Client GET /videos/{id}/stream.
- API return manifest URL (CDN).
- Client request manifest from CDN.
- CDN serve from edge cache (or pull from origin).
- Client download segments adaptively.
- Track view progress, quality changes.
- 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
| Format | Pros | Cons |
|---|---|---|
| HLS | Widely supported, Apple ecosystem | Apple-controlled, H.264 only |
| DASH | Open standard, codec-agnostic | Less 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
-
Clarify requirements (2-3 phút)
- Ask về functional requirements.
- Ask về scale (users, RPS, data size).
- Confirm non-functional requirements (latency, availability).
-
Estimation (2 phút)
- Calculate RPS, storage, bandwidth.
- Identify read-heavy vs write-heavy.
-
High-level design (5 phút)
- Draw block diagram với major components.
- Explain data flow.
-
Detailed design (10 phút)
- Database schema.
- API design.
- Key algorithms.
-
Identify bottlenecks (3 phút)
- Single points of failure.
- Scalability issues.
- Performance optimizations.
-
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
- Designing Data-Intensive Applications - Martin Kleppmann
- System Design Interview - Alex Xu
Online Resources
Practice Platforms
- Pramp - Mock interviews
- Interviewing.io - Anonymous mock interviews
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! 🎉