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;