Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

SQL Server Programming (T-SQL Programming)

Phần này tổng hợp kiến thức về lập trình T-SQL trong SQL Server, từ các khái niệm cơ bản đến kỹ thuật nâng cao. Đây là nền tảng quan trọng để xây dựng các ứng dụng database hiệu năng cao và bảo trì dễ dàng.

Các chủ đề chính

Chủ đềMô tả
Stored ProceduresThủ tục lưu trữ, tham số, dynamic SQL, error handling, transactions
User-Defined FunctionsScalar, Inline TVF, Multi-Statement TVF, performance considerations
TriggersDML/DDL/Logon triggers, inserted/deleted tables, anti-patterns
Views, CTEs & Window FunctionsViews, indexed views, CTEs, recursive queries, window functions, cursors

Tổng quan T-SQL Programming

Database Objects trong SQL Server

SQL Server Database Objects
├── Programmability
│   ├── Stored Procedures
│   ├── Functions
│   │   ├── Scalar Functions
│   │   ├── Table-Valued Functions
│   │   └── Aggregate Functions (CLR)
│   ├── Triggers
│   │   ├── DML Triggers
│   │   ├── DDL Triggers
│   │   └── Logon Triggers
│   └── Types
├── Views
│   ├── Standard Views
│   └── Indexed Views (Materialized)
└── Queries
    ├── CTEs (Common Table Expressions)
    ├── Window Functions
    └── Cursors

Khi nào dùng gì?

Tình huốngGiải pháp tốt nhất
Tái sử dụng business logic phức tạpStored Procedure
Tính toán trong SELECT, tái sử dụngInline TVF hoặc Scalar UDF (SQL 2019+)
Tự động audit/validate khi DMLTrigger (thận trọng)
Đơn giản hóa truy vấn phức tạpView hoặc CTE
Aggregation theo partitionWindow Functions
Traversal hierarchyRecursive CTE
Pre-aggregate dữ liệu lớnIndexed View

Q&A - Phỏng vấn T-SQL Programming

🟢 Junior Level

Q1: Stored Procedure là gì? Lợi ích so với việc gửi raw SQL từ application?

A: Stored Procedure (SP) là một tập hợp các câu lệnh T-SQL được biên dịch và lưu trong database. Lợi ích:

  • Code reuse: Nhiều application/user dùng chung logic
  • Security: Cấp quyền EXECUTE thay vì SELECT/INSERT/UPDATE trực tiếp
  • Performance: Execution plan được cache lần đầu, các lần sau tái sử dụng
  • Reduced network traffic: Chỉ gửi tên SP và tham số thay vì toàn bộ SQL text
  • Maintainability: Thay đổi logic ở một nơi
-- Thay vì gửi raw SQL từ app:
-- SELECT * FROM Orders WHERE CustomerId = 1 AND Status = 'Active'

-- Dùng SP:
EXEC sp_GetActiveOrders @CustomerId = 1;

Q2: Sự khác nhau giữa Scalar Function và Table-Valued Function?

A:

Tiêu chíScalar FunctionTable-Valued Function
Trả vềMột giá trị đơnMột tập kết quả (table)
Dùng trongSELECT, WHERE, JOINFROM clause
PerformanceChậm (row-by-row)Nhanh hơn (set-based với iTVF)
Ví dụdbo.GetAge(BirthDate)dbo.GetOrdersByCustomer(1)

Q3: Trigger là gì? Có mấy loại trigger trong SQL Server?

A: Trigger là một loại stored procedure đặc biệt, tự động thực thi khi xảy ra một sự kiện nhất định.

  • DML Triggers: Kích hoạt bởi INSERT, UPDATE, DELETE
    • AFTER trigger: Chạy sau khi DML hoàn thành
    • INSTEAD OF trigger: Thay thế DML gốc
  • DDL Triggers: Kích hoạt bởi CREATE, ALTER, DROP
  • Logon Triggers: Kích hoạt khi user đăng nhập

Q4: View là gì? Khi nào nên dùng View?

A: View là một virtual table được định nghĩa bởi một câu SELECT. View không lưu dữ liệu (trừ Indexed View).

Nên dùng khi:

  • Đơn giản hóa câu truy vấn phức tạp cho user
  • Ẩn các cột nhạy cảm (security layer)
  • Cung cấp interface ổn định khi schema thay đổi
  • Tổng hợp dữ liệu từ nhiều bảng thường xuyên truy vấn

Q5: CTE là gì? Cú pháp cơ bản?

A: CTE (Common Table Expression) là một named temporary result set được định nghĩa trong phạm vi của một câu query.

WITH CTE_Name AS (
    SELECT column1, column2
    FROM TableName
    WHERE condition
)
SELECT * FROM CTE_Name;

CTE giúp code dễ đọc hơn, thay thế subquery phức tạp.


Q6: Window Function là gì? Cho ví dụ ROW_NUMBER()?

A: Window Function thực hiện tính toán trên một tập các rows liên quan đến row hiện tại (một “window”), không làm giảm số rows kết quả như GROUP BY.

SELECT
    EmployeeId,
    Department,
    Salary,
    ROW_NUMBER() OVER (PARTITION BY Department ORDER BY Salary DESC) AS RankInDept
FROM Employees;

Q7: Sự khác nhau giữa RANK(), DENSE_RANK() và ROW_NUMBER()?

A:

-- Dữ liệu: Salary = 5000, 4000, 4000, 3000
SELECT
    Salary,
    ROW_NUMBER() OVER (ORDER BY Salary DESC) AS RowNum,   -- 1, 2, 3, 4
    RANK()       OVER (ORDER BY Salary DESC) AS Rnk,      -- 1, 2, 2, 4
    DENSE_RANK() OVER (ORDER BY Salary DESC) AS DenseRnk  -- 1, 2, 2, 3
FROM Salaries;
  • ROW_NUMBER: Luôn unique, không có ties
  • RANK: Ties có cùng rank, skip numbers sau đó
  • DENSE_RANK: Ties có cùng rank, không skip numbers

Q8: Cursor là gì? Tại sao nên tránh dùng cursor?

A: Cursor cho phép xử lý từng row một (row-by-row) thay vì set-based. Nên tránh vì:

  • Chậm hơn set-based operations nhiều lần
  • Tốn tài nguyên (memory, temp storage)
  • Gây lock lâu hơn
  • Hầu hết trường hợp đều có thể thay bằng set-based SQL hoặc window functions

Q9: DECLARE và SET khác nhau như thế nào với SELECT khi gán biến?

A:

DECLARE @Name NVARCHAR(100);

-- SET: chỉ gán một giá trị, nếu query trả về nhiều row thì lỗi
SET @Name = (SELECT Name FROM Employees WHERE Id = 1);

-- SELECT: gán giá trị cuối cùng nếu nhiều rows
SELECT @Name = Name FROM Employees WHERE DepartmentId = 5;
-- Nếu không có row nào thỏa điều kiện:
-- SET -> @Name = NULL
-- SELECT -> @Name giữ nguyên giá trị cũ (nguy hiểm!)

Q10: @@ROWCOUNT và @@ERROR dùng để làm gì?

A:

  • @@ROWCOUNT: Số dòng bị ảnh hưởng bởi câu lệnh cuối cùng
  • @@ERROR: Error number của lỗi cuối (0 nếu không có lỗi)
UPDATE Orders SET Status = 'Shipped' WHERE OrderId = 100;
IF @@ROWCOUNT = 0
    PRINT 'Không tìm thấy Order';

Lưu ý: @@ERROR bị reset sau mỗi câu lệnh. Dùng TRY...CATCH thay thế trong code hiện đại.


🟡 Mid Level

Q11: Parameter Sniffing trong Stored Procedure là gì? Cách xử lý?

A: Parameter Sniffing là cơ chế SQL Server compile execution plan dựa trên giá trị tham số đầu tiên được sử dụng. Plan tốt cho giá trị đó có thể rất tệ cho giá trị khác.

-- Vấn đề: Compile với @CustomerId = 1 (ít orders)
-- Plan dùng Index Seek. Nhưng khi gọi với @CustomerId = 999 (nhiều orders)
-- vẫn dùng plan cũ -> chậm

CREATE PROCEDURE GetCustomerOrders @CustomerId INT
AS
    SELECT * FROM Orders WHERE CustomerId = @CustomerId;

-- Giải pháp 1: OPTION (RECOMPILE) - recompile mỗi lần
CREATE PROCEDURE GetCustomerOrders @CustomerId INT
AS
    SELECT * FROM Orders WHERE CustomerId = @CustomerId
    OPTION (RECOMPILE);

-- Giải pháp 2: Local variables (ngăn sniffing, mất một số optimization)
CREATE PROCEDURE GetCustomerOrders @CustomerId INT
AS
    DECLARE @LocalCustomerId INT = @CustomerId;
    SELECT * FROM Orders WHERE CustomerId = @LocalCustomerId;

-- Giải pháp 3: WITH RECOMPILE ở SP level
CREATE PROCEDURE GetCustomerOrders @CustomerId INT
WITH RECOMPILE
AS
    SELECT * FROM Orders WHERE CustomerId = @CustomerId;

Q12: Dynamic SQL là gì? sp_executesql có lợi ích gì so với EXEC()?

A: Dynamic SQL là SQL được xây dựng và thực thi lúc runtime.

-- EXEC() - không parameterized, dễ SQL injection
EXEC ('SELECT * FROM ' + @TableName + ' WHERE Id = ' + @Id);

-- sp_executesql - parameterized, safe, plan reuse
DECLARE @SQL NVARCHAR(MAX);
DECLARE @Params NVARCHAR(500);

SET @SQL = N'SELECT * FROM Orders WHERE CustomerId = @CustId AND Status = @Status';
SET @Params = N'@CustId INT, @Status NVARCHAR(50)';

EXEC sp_executesql @SQL, @Params, 
    @CustId = @CustomerId, 
    @Status = @OrderStatus;

Lợi ích của sp_executesql:

  1. Parameterized: Ngăn SQL injection
  2. Plan caching: Cùng SQL text → tái sử dụng execution plan
  3. Output parameters: Có thể nhận giá trị trả về

Q13: Indexed View (Materialized View) là gì? Yêu cầu để tạo Indexed View?

A: Indexed View là view có dữ liệu được vật lý hóa (stored) trên disk, cập nhật tự động khi base tables thay đổi.

Yêu cầu bắt buộc:

  1. WITH SCHEMABINDING trong CREATE VIEW
  2. Unique Clustered Index là index đầu tiên tạo trên view
  3. Các function phải deterministic
  4. Không dùng *, DISTINCT, TOP, OUTER JOIN, subqueries, CTEs
  5. SET ANSI_NULLS ONSET QUOTED_IDENTIFIER ON
CREATE VIEW dbo.vw_SalesSummary
WITH SCHEMABINDING
AS
    SELECT 
        p.CategoryId,
        COUNT_BIG(*) AS TotalOrders,
        SUM(od.Quantity * od.UnitPrice) AS TotalRevenue
    FROM dbo.OrderDetails od
    INNER JOIN dbo.Products p ON od.ProductId = p.ProductId
    GROUP BY p.CategoryId;
GO

-- Tạo unique clustered index để materialize
CREATE UNIQUE CLUSTERED INDEX IX_vw_SalesSummary
ON dbo.vw_SalesSummary (CategoryId);

Q14: Recursive CTE hoạt động như thế nào? Viết query duyệt cây phân cấp?

A: Recursive CTE gồm hai phần:

  • Anchor member: Query không đệ quy, trả về điểm bắt đầu
  • Recursive member: Query tham chiếu chính CTE đó
-- Bảng phân cấp nhân viên
-- EmployeeId, Name, ManagerId

WITH EmployeeHierarchy AS (
    -- Anchor: CEO (không có manager)
    SELECT 
        EmployeeId, Name, ManagerId,
        0 AS Level,
        CAST(Name AS NVARCHAR(MAX)) AS Path
    FROM Employees
    WHERE ManagerId IS NULL

    UNION ALL

    -- Recursive: tìm direct reports
    SELECT 
        e.EmployeeId, e.Name, e.ManagerId,
        eh.Level + 1,
        eh.Path + ' > ' + e.Name
    FROM Employees e
    INNER JOIN EmployeeHierarchy eh ON e.ManagerId = eh.EmployeeId
)
SELECT EmployeeId, Name, Level, Path
FROM EmployeeHierarchy
OPTION (MAXRECURSION 100); -- giới hạn độ sâu đệ quy

Q15: Sự khác nhau giữa AFTER trigger và INSTEAD OF trigger?

A:

AFTER TriggerINSTEAD OF Trigger
Thời điểmSau khi DML hoàn thànhThay thế DML, không chạy DML gốc
Có thể rollback?Có (nằm trong cùng transaction)Phải tự thực hiện DML nếu muốn
Áp dụng trênTablesTables và Views
Dùng choAudit, cascade business rulesUpdatable views, complex validation
-- INSTEAD OF trigger trên view cho phép update
CREATE TRIGGER trg_InsertOrderSummary
ON vw_OrderSummary
INSTEAD OF INSERT
AS
BEGIN
    -- Tự viết logic INSERT vào base tables
    INSERT INTO Orders (CustomerId, OrderDate)
    SELECT CustomerId, OrderDate FROM inserted;
END;

Q16: LAG() và LEAD() dùng để làm gì? Cho ví dụ tính month-over-month growth?

A:

-- LAG: lấy giá trị từ row trước
-- LEAD: lấy giá trị từ row sau
SELECT
    Month,
    Revenue,
    LAG(Revenue, 1, 0) OVER (ORDER BY Month) AS PrevMonthRevenue,
    LEAD(Revenue, 1, 0) OVER (ORDER BY Month) AS NextMonthRevenue,
    Revenue - LAG(Revenue, 1, 0) OVER (ORDER BY Month) AS MoMChange,
    CASE 
        WHEN LAG(Revenue, 1, 0) OVER (ORDER BY Month) = 0 THEN NULL
        ELSE CAST(
            (Revenue - LAG(Revenue, 1, 0) OVER (ORDER BY Month)) * 100.0 
            / LAG(Revenue, 1, 0) OVER (ORDER BY Month) 
            AS DECIMAL(10,2))
    END AS MoMGrowthPct
FROM MonthlySales
ORDER BY Month;

Q17: Running Total (Cumulative Sum) tính như thế nào với Window Functions?

A:

SELECT
    OrderDate,
    OrderAmount,
    SUM(OrderAmount) OVER (
        ORDER BY OrderDate
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS RunningTotal
FROM Orders;

-- So sánh ROWS vs RANGE:
-- ROWS BETWEEN: tính theo vị trí vật lý (exact)
-- RANGE BETWEEN: tính theo giá trị (bao gồm ties)
-- ROWS thường hiệu năng tốt hơn

Q18: Khi nào dùng CTE, khi nào dùng Temp Table, khi nào dùng Table Variable?

A:

CTETemp TableTable Variable
ScopeTrong querySessionBatch/SP
StatisticsKhông cóCó (auto)Không có (trước 2019)
IndexKhôngCó thể tạoChỉ PK/UK
Reuse nhiều lầnKhông (re-evaluate)
Transaction logKhông riêng biệtTempDBTempDB
Lớn hơn 1000 rowsNên dùng Temp Table
Recursive

Q19: SCHEMABINDING trong View và Function có tác dụng gì?

A: WITH SCHEMABINDING ràng buộc object (view/function) với schema của các base objects.

  • Không thể DROP hoặc ALTER base tables/columns khi chưa DROP view/function trước
  • Bắt buộc để tạo Indexed View
  • Cho phép function/view được đánh dấu là deterministic → có thể dùng trong computed column, index
-- Không thể DROP Products.Price nếu còn view này
CREATE VIEW dbo.vw_ProductPrices
WITH SCHEMABINDING
AS
    SELECT ProductId, ProductName, Price
    FROM dbo.Products; -- Phải dùng schema name (dbo.)

Q20: TRY…CATCH trong T-SQL hoạt động như thế nào?

A:

BEGIN TRY
    BEGIN TRANSACTION;
    
    INSERT INTO Orders (CustomerId, Amount) VALUES (1, 500);
    UPDATE Inventory SET Stock = Stock - 1 WHERE ProductId = 10;
    
    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    IF @@TRANCOUNT > 0
        ROLLBACK TRANSACTION;
    
    -- Lấy thông tin lỗi
    SELECT
        ERROR_NUMBER()    AS ErrorNumber,
        ERROR_MESSAGE()   AS ErrorMessage,
        ERROR_SEVERITY()  AS Severity,
        ERROR_STATE()     AS State,
        ERROR_LINE()      AS ErrorLine,
        ERROR_PROCEDURE() AS ErrorProcedure;
    
    -- Re-throw lỗi (SQL 2012+)
    THROW;
END CATCH;

Q21: RAISERROR và THROW khác nhau như thế nào?

A:

RAISERRORTHROW
Cú phápRAISERROR(msg, severity, state)THROW [error_number, message, state]
SeverityCần chỉ địnhMặc định 16
Re-throwRAISERROR với @ErrorMessageTHROW; (không tham số)
Error number tùy chỉnhCần trong sys.messagesBất kỳ số >= 50000
SQL versionSQL 2012+ (khuyến dùng)
-- THROW re-throw trong CATCH
BEGIN CATCH
    ROLLBACK TRANSACTION;
    THROW; -- Re-throws original error với full context
END CATCH;

-- RAISERROR custom message
RAISERROR('Số lượng không được âm', 16, 1);

-- THROW custom
THROW 50001, 'Số lượng không được âm', 1;

🔴 Senior Level

Q22: Giải thích vấn đề “Halloween Protection” trong SQL Server và ảnh hưởng đến trigger?

A: Halloween Protection là cơ chế SQL Server đảm bảo rằng một row không bị thay đổi nhiều lần trong cùng một câu DML. SQL Server đôi khi phải dùng eager spool operator, có thể ảnh hưởng hiệu năng.

Với trigger, inserteddeleted tables chứa snapshot của rows bị ảnh hưởng, không phải toàn bộ bảng. Trigger phải xử lý multi-row operations:

-- Anti-pattern: Giả sử chỉ một row bị UPDATE
CREATE TRIGGER trg_AfterUpdate ON Orders AFTER UPDATE
AS
BEGIN
    DECLARE @OrderId INT = (SELECT OrderId FROM inserted); -- SAI khi nhiều rows!
    UPDATE OrderAudit SET ModifiedDate = GETDATE() WHERE OrderId = @OrderId;
END;

-- Đúng: Xử lý set-based
CREATE TRIGGER trg_AfterUpdate ON Orders AFTER UPDATE
AS
BEGIN
    UPDATE oa
    SET ModifiedDate = GETDATE()
    FROM OrderAudit oa
    INNER JOIN inserted i ON oa.OrderId = i.OrderId;
END;

Q23: Scalar UDF Inlining trong SQL Server 2019 là gì? Điều kiện để một UDF được inline?

A: Scalar UDF Inlining là tính năng SQL 2019 tự động chuyển đổi eligible scalar UDFs thành relational expressions (như inline views), cho phép:

  • Parallelism (trước đây UDF bắt query dùng single-thread)
  • Cost-based optimization
  • Push predicates vào UDF

Điều kiện để được inline:

  1. Chỉ có một RETURN statement
  2. Không dùng EXECUTE
  3. Không dùng recursive calls
  4. Không dùng TRY...CATCH
  5. Không dùng table variables
  6. Không có side effects (chỉ SELECT)
  7. Không gọi các functions không deterministic như RAND(), NEWID()
-- Kiểm tra UDF có được inline không
SELECT 
    name,
    is_inlineable
FROM sys.sql_modules sm
JOIN sys.objects o ON sm.object_id = o.object_id
WHERE o.type = 'FN';

-- Force inline (nếu không eligible tự động)
SELECT dbo.GetDiscount(ProductId) WITH INLINE = ON
-- hoặc disable:
SELECT dbo.GetDiscount(ProductId) WITH INLINE = OFF

Q24: Giải thích ROWS BETWEEN vs RANGE BETWEEN trong Window Functions. Performance implications?

A:

-- ROWS: frame theo vị trí vật lý (exact row count)
SUM(Amount) OVER (ORDER BY Date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)

-- RANGE: frame theo giá trị (bao gồm tất cả rows cùng giá trị ORDER BY)
SUM(Amount) OVER (ORDER BY Date RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)

Sự khác biệt với ties:

  • Nếu có nhiều rows cùng Date:
    • ROWS: Running total tính đến chính xác row hiện tại
    • RANGE: Running total bao gồm tất cả rows cùng Date

Performance:

  • RANGE BETWEEN yêu cầu spool operator (lưu tạm kết quả) → chậm hơn
  • ROWS BETWEEN hiệu quả hơn, ít tốn memory hơn
  • Khi không chỉ định: default là RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW → nên luôn explicit dùng ROWS

Q25: CDC (Change Data Capture) vs Triggers: khi nào dùng cái nào?

A:

TriggersCDC
ImplementationT-SQL manualSQL Server built-in
OverheadSynchronous, trong transactionAsynchronous, đọc transaction log
Impact on DMLCó (tăng latency)Minimal (async)
GranularityRow + column level tùy chọnRow + column level
HistoryPhải tự buildBuilt-in change table
SetupĐơn giảnPhức tạp hơn
Use caseReal-time validation/cascadeAudit, ETL, data sync

CDC phù hợp hơn khi:

  • Cần audit history mà không muốn ảnh hưởng performance
  • ETL pipeline cần biết changed data
  • Replication-like scenarios

Trigger phù hợp hơn khi:

  • Cần real-time validation/enforcement
  • Cần cascade business logic ngay lập tức
  • Cần reject DML dựa trên điều kiện phức tạp

Q26: SAVE TRANSACTION (Savepoint) hoạt động như thế nào? Khi nào dùng?

A:

BEGIN TRANSACTION OuterTran;

    INSERT INTO AuditLog (Action) VALUES ('Start process');

    SAVE TRANSACTION SavePoint1; -- Tạo savepoint

    BEGIN TRY
        INSERT INTO Orders (CustomerId) VALUES (999); -- Có thể lỗi
        
        SAVE TRANSACTION SavePoint2;
        
        UPDATE Inventory SET Stock = Stock - 1 WHERE ProductId = 1;
        
    END TRY
    BEGIN CATCH
        -- Rollback về savepoint, không rollback toàn bộ
        ROLLBACK TRANSACTION SavePoint1;
        -- AuditLog INSERT vẫn còn
        INSERT INTO AuditLog (Action) VALUES ('Order failed: ' + ERROR_MESSAGE());
    END CATCH;

COMMIT TRANSACTION OuterTran;

Lưu ý quan trọng:

  • ROLLBACK TRANSACTION SavepointName chỉ undo đến savepoint, không kết thúc transaction
  • @@TRANCOUNT không thay đổi khi rollback về savepoint
  • Savepoint không thực sự “commit” data, data chỉ commit khi outer transaction commit

Q27: COLUMNS_UPDATED() và UPDATE() function trong trigger dùng như thế nào?

A:

CREATE TRIGGER trg_OrderUpdate ON Orders AFTER UPDATE
AS
BEGIN
    -- UPDATE(column): kiểm tra column có trong UPDATE statement không
    IF UPDATE(Status) OR UPDATE(Amount)
    BEGIN
        INSERT INTO OrderAuditLog (OrderId, ChangedBy, ChangeTime)
        SELECT i.OrderId, SYSTEM_USER, GETDATE()
        FROM inserted i;
    END;

    -- COLUMNS_UPDATED(): bitmap của columns được update
    -- Ít dùng hơn vì phức tạp, phụ thuộc vào ordinal position
    IF (COLUMNS_UPDATED() & 4) = 4 -- Column thứ 3 (bit 3 = 4)
    BEGIN
        -- Column 3 được update
    END;
END;

Q28: Nested Stored Procedures và @@NESTLEVEL. Giới hạn là bao nhiêu?

A:

  • SQL Server cho phép tối đa 32 levels của nested procedure calls
  • @@NESTLEVEL trả về level hiện tại (0 = top level, 1 = called from top-level SP, …)
CREATE PROCEDURE dbo.OuterProc
AS
BEGIN
    SELECT @@NESTLEVEL; -- Returns 1
    EXEC dbo.InnerProc;
END;

CREATE PROCEDURE dbo.InnerProc
AS
BEGIN
    SELECT @@NESTLEVEL; -- Returns 2
    
    IF @@NESTLEVEL > 16
    BEGIN
        THROW 50001, 'Quá nhiều nested calls', 1;
    END;
END;

Q29: Gaps and Islands problem - giải quyết bằng Window Functions?

A: Bài toán tìm các dải liên tiếp (islands) và khoảng trống (gaps) trong dữ liệu.

-- Dữ liệu: LoginDate của user
-- Tìm các "dải" ngày liên tiếp user login

WITH LoginData AS (
    SELECT 
        UserId,
        LoginDate,
        ROW_NUMBER() OVER (PARTITION BY UserId ORDER BY LoginDate) AS RowNum
    FROM UserLogins
),
Islands AS (
    SELECT
        UserId,
        LoginDate,
        DATEADD(DAY, -RowNum, LoginDate) AS IslandGroup -- Ngày trừ đi row number
    FROM LoginData
)
SELECT
    UserId,
    MIN(LoginDate) AS IslandStart,
    MAX(LoginDate) AS IslandEnd,
    DATEDIFF(DAY, MIN(LoginDate), MAX(LoginDate)) + 1 AS ConsecutiveDays
FROM Islands
GROUP BY UserId, IslandGroup
ORDER BY UserId, IslandStart;

Q30: Giải thích “deferred name resolution” trong Stored Procedures. Ảnh hưởng gì?

A: SQL Server không verify tên của objects trong stored procedure tại thời điểm CREATE PROCEDURE, mà chỉ resolve khi SP được thực thi. Điều này có nghĩa:

-- Tạo SP thành công dù NonExistentTable chưa tồn tại
CREATE PROCEDURE dbo.MyProc
AS
    SELECT * FROM NonExistentTable; -- Không lỗi khi CREATE
GO

-- Chỉ lỗi khi EXEC
EXEC dbo.MyProc; -- Lỗi: Invalid object name 'NonExistentTable'

Ảnh hưởng:

  • Cho phép tạo SP trước khi tables tồn tại (useful trong deployment scenarios)
  • Có thể tạo ra lỗi runtime khó debug
  • Ngoại lệ: Column names và data types được resolve lúc compile → lỗi nếu column không tồn tại

Best practice: Dùng sys.sql_modules hoặc sp_refreshsqlmodule để refresh dependencies sau khi schema thay đổi.


Q31: Viết query phân trang (pagination) hiệu năng cao với Window Functions?

A:

-- Cách 1: OFFSET/FETCH (SQL 2012+) - đơn giản nhưng chậm với large offset
SELECT *
FROM Products
ORDER BY ProductId
OFFSET (@Page - 1) * @PageSize ROWS
FETCH NEXT @PageSize ROWS ONLY;

-- Cách 2: Keyset pagination (Seek method) - hiệu năng cao
-- Dùng giá trị cuối của page trước thay vì OFFSET
SELECT TOP (@PageSize) *
FROM Products
WHERE ProductId > @LastProductId -- @LastProductId = ProductId cuối page trước
ORDER BY ProductId;

-- Cách 3: ROW_NUMBER() approach
WITH PagedData AS (
    SELECT
        *,
        ROW_NUMBER() OVER (ORDER BY ProductId) AS RowNum
    FROM Products
)
SELECT *
FROM PagedData
WHERE RowNum BETWEEN (@Page - 1) * @PageSize + 1 AND @Page * @PageSize;

So sánh hiệu năng:

  • OFFSET/FETCH: Tốt cho small-medium datasets, dễ implement
  • Keyset: Tốt nhất cho large datasets, nhưng không hỗ trợ random page access
  • ROW_NUMBER(): Linh hoạt nhưng phải tính toán toàn bộ resultset

Q32: Sự khác nhau giữa FAST_FORWARD cursor và SET-BASED operations. Khi nào cursor thực sự cần thiết?

A: FAST_FORWARD là cursor type nhanh nhất (read-only, forward-only), nhưng vẫn chậm hơn set-based nhiều.

Cursor thực sự cần khi:

  1. Gọi Stored Procedure cho mỗi row (không thể set-based)
  2. Cần gửi kết quả row-by-row qua network stream
  3. Logic quá phức tạp không thể biểu diễn bằng set-based
-- Cursor FAST_FORWARD (nhanh nhất nếu phải dùng cursor)
DECLARE @OrderId INT;

DECLARE cur CURSOR FAST_FORWARD FOR
    SELECT OrderId FROM Orders WHERE Status = 'Pending';

OPEN cur;
FETCH NEXT FROM cur INTO @OrderId;

WHILE @@FETCH_STATUS = 0
BEGIN
    EXEC ProcessOrder @OrderId; -- Gọi SP cho từng row
    FETCH NEXT FROM cur INTO @OrderId;
END;

CLOSE cur;
DEALLOCATE cur;

-- Thay thế bằng: EXECUTE dùng STRING_AGG hoặc XML để batch

Q33: Giải thích cách SQL Server cache execution plans. Khi nào plan bị invalidated?

A:

Plan caching mechanism:

  1. SQL text được hash → tìm trong plan cache
  2. Nếu tìm thấy → reuse (soft parse)
  3. Nếu không → compile → lưu vào cache (hard parse)

Plan bị invalidated/evicted khi:

  • Statistics được update (UPDATE STATISTICS)
  • Index được tạo/drop/rebuild
  • sp_recompile được gọi
  • DBCC FREEPROCCACHE được chạy
  • Bộ nhớ áp lực cao (cache eviction)
  • Schema của referenced objects thay đổi
  • SET options thay đổi (ANSI_NULLS, etc.)
-- Xem plan cache
SELECT 
    qs.execution_count,
    qs.total_elapsed_time / qs.execution_count AS avg_elapsed_time,
    SUBSTRING(qt.text, qs.statement_start_offset/2, 
        (CASE WHEN qs.statement_end_offset = -1 
              THEN LEN(CONVERT(NVARCHAR(MAX), qt.text)) * 2 
              ELSE qs.statement_end_offset END - qs.statement_start_offset)/2) AS query_text,
    qp.query_plan
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) qp
ORDER BY avg_elapsed_time DESC;