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

Views, CTEs & Window Functions

Phần này bao gồm các kỹ thuật truy vấn nâng cao trong SQL Server: Views (bao gồm Indexed Views), CTEs (Common Table Expressions), Window Functions, và Cursors.


PHẦN 1: Views

1.1 Tạo và Quản Lý Views

-- Tạo view cơ bản
CREATE VIEW dbo.vw_ActiveOrders
AS
SELECT 
    o.OrderId,
    o.OrderDate,
    o.TotalAmount,
    o.Status,
    c.CustomerName,
    c.Email,
    c.Region
FROM dbo.Orders o
INNER JOIN dbo.Customers c ON o.CustomerId = c.CustomerId
WHERE o.IsDeleted = 0;
GO

-- Sửa view (giữ nguyên permissions)
ALTER VIEW dbo.vw_ActiveOrders
AS
SELECT 
    o.OrderId,
    o.OrderDate,
    o.TotalAmount,
    o.Status,
    c.CustomerName,
    c.Email,
    c.Region,
    o.ShippedDate -- Thêm cột mới
FROM dbo.Orders o
INNER JOIN dbo.Customers c ON o.CustomerId = c.CustomerId
WHERE o.IsDeleted = 0 AND o.Status <> 'Cancelled';
GO

-- Xóa view
DROP VIEW IF EXISTS dbo.vw_ActiveOrders;

-- Xem định nghĩa view
SELECT OBJECT_DEFINITION(OBJECT_ID('dbo.vw_ActiveOrders'));
-- hoặc
EXEC sp_helptext 'dbo.vw_ActiveOrders';

1.2 WITH SCHEMABINDING

-- SCHEMABINDING: Ngăn thay đổi base objects
-- BẮT BUỘC cho Indexed Views
-- Yêu cầu: Phải dùng tên đầy đủ (schema.table)

CREATE VIEW dbo.vw_ProductPriceList
WITH SCHEMABINDING -- Khóa schema
AS
SELECT 
    p.ProductId,
    p.ProductName,
    p.Price,
    c.CategoryName
FROM dbo.Products p           -- Phải có dbo.
INNER JOIN dbo.Categories c ON p.CategoryId = c.CategoryId
WHERE p.IsActive = 1;
GO

-- Thử DROP column của base table → LỖI
-- ALTER TABLE dbo.Products DROP COLUMN Price;
-- ERROR: Cannot DROP COLUMN 'Price' because it is referenced by view 'vw_ProductPriceList'

-- Kiểm tra view có SCHEMABINDING không
SELECT 
    v.name,
    v.with_check_option,
    m.is_schema_bound
FROM sys.views v
JOIN sys.sql_modules m ON v.object_id = m.object_id
WHERE v.name = 'vw_ProductPriceList';

1.3 WITH CHECK OPTION

-- CHECK OPTION: Đảm bảo INSERT/UPDATE qua view phải thỏa WHERE của view
CREATE VIEW dbo.vw_ActiveProducts
AS
SELECT ProductId, ProductName, Price, IsActive
FROM dbo.Products
WHERE IsActive = 1
WITH CHECK OPTION; -- Ngăn INSERT/UPDATE tạo ra rows không visible trong view

-- INSERT thỏa điều kiện → OK
INSERT INTO dbo.vw_ActiveProducts (ProductName, Price, IsActive)
VALUES ('New Product', 99.99, 1);

-- INSERT vi phạm điều kiện → ERROR
INSERT INTO dbo.vw_ActiveProducts (ProductName, Price, IsActive)
VALUES ('Inactive Product', 99.99, 0);
-- ERROR: The attempted insert or update failed because the target view
-- either specifies WITH CHECK OPTION or spans a view that specifies WITH CHECK OPTION

1.4 Updatable Views

View có thể UPDATE/INSERT/DELETE nếu thỏa:

Ràng buộcMô tả
Không có DISTINCT
Không có aggregate (SUM, COUNT…)
Không có GROUP BY, HAVING
Không có TOP hoặc UNION
Chỉ reference một base table trong DML
-- View đơn giản → updatable
CREATE VIEW dbo.vw_EmployeeNames AS
SELECT EmployeeId, FirstName, LastName FROM Employees;

-- UPDATE trực tiếp qua view
UPDATE dbo.vw_EmployeeNames 
SET LastName = 'Nguyen' 
WHERE EmployeeId = 1;
-- Tương đương: UPDATE Employees SET LastName = 'Nguyen' WHERE EmployeeId = 1

-- View join nhiều bảng → KHÔNG updatable trực tiếp
-- Dùng INSTEAD OF trigger (xem phần Triggers)

1.5 Indexed Views (Materialized Views)

Indexed View lưu kết quả vật lý trên disk, tự động cập nhật khi base tables thay đổi.

-- Bước 1: Tạo view với SCHEMABINDING (bắt buộc)
CREATE VIEW dbo.vw_DailySalesSummary
WITH SCHEMABINDING
AS
SELECT 
    CAST(o.OrderDate AS DATE) AS SaleDate,
    p.CategoryId,
    COUNT_BIG(*) AS OrderCount,      -- Phải dùng COUNT_BIG(*), không phải COUNT(*)
    SUM(od.Quantity * od.UnitPrice) AS TotalRevenue,
    SUM(CAST(od.Quantity AS BIGINT)) AS TotalQuantity
FROM dbo.Orders o
INNER JOIN dbo.OrderDetails od ON o.OrderId = od.OrderId
INNER JOIN dbo.Products p ON od.ProductId = p.ProductId
WHERE o.IsDeleted = 0
GROUP BY CAST(o.OrderDate AS DATE), p.CategoryId;
GO

-- Bước 2: Tạo UNIQUE CLUSTERED INDEX → materialize view
CREATE UNIQUE CLUSTERED INDEX UCIX_vw_DailySalesSummary
ON dbo.vw_DailySalesSummary (SaleDate, CategoryId);
GO

-- Bước 3 (Optional): Thêm non-clustered indexes
CREATE INDEX IX_vw_DailySalesSummary_Revenue
ON dbo.vw_DailySalesSummary (TotalRevenue DESC);

-- Optimizer tự động dùng indexed view khi profitable
-- (với ENTERPRISE edition, STANDARD cần NOEXPAND hint)

-- Force dùng indexed view (STANDARD edition)
SELECT SaleDate, CategoryId, TotalRevenue
FROM dbo.vw_DailySalesSummary WITH (NOEXPAND)
WHERE SaleDate = '2025-01-01';

Yêu cầu cho Indexed View:

Yêu cầuChi tiết
WITH SCHEMABINDINGBắt buộc
First index là UNIQUE CLUSTEREDBắt buộc
Không có *Phải explicit columns
Không DISTINCT, TOP, subqueries
Không OUTER JOINChỉ INNER JOIN
COUNT_BIG(*) thay vì COUNT(*)Khi có GROUP BY
Deterministic functions only
SET ANSI_NULLS ONSET QUOTED_IDENTIFIER ON

1.6 sys.views và INFORMATION_SCHEMA.VIEWS

-- Liệt kê tất cả views
SELECT 
    v.name AS ViewName,
    s.name AS SchemaName,
    v.create_date,
    v.modify_date,
    m.is_schema_bound,
    m.uses_ansi_nulls
FROM sys.views v
JOIN sys.schemas s ON v.schema_id = s.schema_id
JOIN sys.sql_modules m ON v.object_id = m.object_id
ORDER BY s.name, v.name;

-- INFORMATION_SCHEMA.VIEWS: Portable across SQL products
SELECT 
    TABLE_SCHEMA,
    TABLE_NAME,
    VIEW_DEFINITION
FROM INFORMATION_SCHEMA.VIEWS
WHERE TABLE_NAME LIKE 'vw_%';

-- Tìm views phụ thuộc vào một bảng
SELECT DISTINCT 
    v.name AS ViewName
FROM sys.views v
JOIN sys.sql_expression_dependencies d ON v.object_id = d.referencing_id
JOIN sys.objects o ON d.referenced_id = o.object_id
WHERE o.name = 'Orders';

PHẦN 2: CTEs (Common Table Expressions)

2.1 Cú Pháp Cơ Bản

-- CTE cơ bản
WITH OrderSummary AS (
    SELECT 
        CustomerId,
        COUNT(*) AS TotalOrders,
        SUM(TotalAmount) AS TotalSpent
    FROM Orders
    WHERE Status = 'Completed'
    GROUP BY CustomerId
)
SELECT 
    c.CustomerName,
    c.Email,
    os.TotalOrders,
    os.TotalSpent
FROM Customers c
INNER JOIN OrderSummary os ON c.CustomerId = os.CustomerId
WHERE os.TotalSpent > 1000
ORDER BY os.TotalSpent DESC;

2.2 Multiple CTEs trong Một Query

WITH
-- CTE 1: High-value customers
HighValueCustomers AS (
    SELECT CustomerId, SUM(TotalAmount) AS TotalSpent
    FROM Orders
    GROUP BY CustomerId
    HAVING SUM(TotalAmount) >= 5000
),
-- CTE 2: Recent orders (dùng bảng gốc)
RecentOrders AS (
    SELECT CustomerId, COUNT(*) AS OrdersLast30Days
    FROM Orders
    WHERE OrderDate >= DATEADD(DAY, -30, GETDATE())
    GROUP BY CustomerId
),
-- CTE 3: Combine (có thể reference các CTEs trước)
CustomerInsights AS (
    SELECT 
        hvc.CustomerId,
        hvc.TotalSpent,
        ISNULL(ro.OrdersLast30Days, 0) AS RecentOrders
    FROM HighValueCustomers hvc
    LEFT JOIN RecentOrders ro ON hvc.CustomerId = ro.CustomerId
)
-- Final query sử dụng CTE cuối cùng
SELECT 
    c.CustomerName,
    ci.TotalSpent,
    ci.RecentOrders,
    CASE 
        WHEN ci.RecentOrders >= 3 THEN 'Very Active'
        WHEN ci.RecentOrders >= 1 THEN 'Active'
        ELSE 'Inactive'
    END AS ActivityStatus
FROM Customers c
INNER JOIN CustomerInsights ci ON c.CustomerId = ci.CustomerId
ORDER BY ci.TotalSpent DESC;

2.3 Recursive CTEs

-- CẤU TRÚC:
-- Anchor Member UNION ALL Recursive Member

-- EXAMPLE 1: Traversal hierarchy nhân viên
WITH EmployeeTree AS (
    -- ANCHOR: CEO (không có manager)
    SELECT 
        EmployeeId,
        FullName,
        ManagerId,
        JobTitle,
        0 AS Level,
        CAST(FullName AS NVARCHAR(MAX)) AS HierarchyPath,
        CAST(EmployeeId AS NVARCHAR(MAX)) AS IdPath
    FROM Employees
    WHERE ManagerId IS NULL

    UNION ALL

    -- RECURSIVE: Direct reports của các employees đã có
    SELECT 
        e.EmployeeId,
        e.FullName,
        e.ManagerId,
        e.JobTitle,
        et.Level + 1 AS Level,
        et.HierarchyPath + ' > ' + e.FullName,
        et.IdPath + '.' + CAST(e.EmployeeId AS NVARCHAR(10))
    FROM Employees e
    INNER JOIN EmployeeTree et ON e.ManagerId = et.EmployeeId
)
SELECT 
    REPLICATE('  ', Level) + FullName AS OrgChart,
    JobTitle,
    Level,
    HierarchyPath
FROM EmployeeTree
ORDER BY IdPath;
-- OPTION (MAXRECURSION 100); -- Giới hạn depth (default là 100, 0 = unlimited)
-- EXAMPLE 2: Đếm ngày làm việc (trừ cuối tuần)
WITH DateSeries AS (
    -- Anchor: ngày bắt đầu
    SELECT CAST('2025-01-01' AS DATE) AS WorkDate

    UNION ALL

    -- Recursive: thêm từng ngày
    SELECT DATEADD(DAY, 1, WorkDate)
    FROM DateSeries
    WHERE WorkDate < '2025-12-31'
)
SELECT COUNT(*) AS WorkingDays
FROM DateSeries
WHERE DATENAME(WEEKDAY, WorkDate) NOT IN ('Saturday', 'Sunday')
OPTION (MAXRECURSION 366);
-- EXAMPLE 3: Tìm routes giữa các nodes (graph traversal)
WITH RouteSearch AS (
    SELECT 
        FromCity, ToCity, 
        Distance AS TotalDistance,
        CAST(FromCity + ' → ' + ToCity AS NVARCHAR(MAX)) AS Route,
        1 AS Hops
    FROM Routes
    WHERE FromCity = 'Hanoi'

    UNION ALL

    SELECT 
        rs.FromCity, r.ToCity,
        rs.TotalDistance + r.Distance,
        rs.Route + ' → ' + r.ToCity,
        rs.Hops + 1
    FROM RouteSearch rs
    INNER JOIN Routes r ON rs.ToCity = r.FromCity
    WHERE rs.Hops < 5 -- Giới hạn số bước
    AND rs.Route NOT LIKE '%' + r.ToCity + '%' -- Tránh cycle
)
SELECT Route, TotalDistance
FROM RouteSearch
WHERE ToCity = 'HCMC'
ORDER BY TotalDistance;

2.4 CTE cho DELETE và UPDATE

-- DELETE với CTE (xóa duplicates)
WITH DuplicateOrders AS (
    SELECT 
        OrderId,
        ROW_NUMBER() OVER (
            PARTITION BY CustomerId, OrderDate, TotalAmount
            ORDER BY OrderId
        ) AS RowNum
    FROM Orders
)
DELETE FROM DuplicateOrders WHERE RowNum > 1;
-- Thực tế: DELETE ảnh hưởng đến bảng Orders gốc

-- UPDATE với CTE (cập nhật với ranking)
WITH RankedSalary AS (
    SELECT 
        EmployeeId,
        Salary,
        DENSE_RANK() OVER (PARTITION BY DepartmentId ORDER BY Salary DESC) AS SalaryRank
    FROM Employees
)
UPDATE RankedSalary
SET Salary = Salary * 1.10 -- Tăng 10% cho top earner
WHERE SalaryRank = 1;

2.5 CTE vs Subquery vs Temp Table

CTESubqueryTemp Table
Readable✅ Rất rõ❌ Lồng nhau✅ Rõ
Reuse❌ Chỉ một lần✅ Nhiều lần
Recursive
Statistics
Large data⚠️⚠️
Index
DELETE/UPDATE

PHẦN 3: Window Functions

3.1 Cú Pháp OVER()

function_name(args) OVER (
    [PARTITION BY partition_expression]
    [ORDER BY sort_expression [ASC|DESC]]
    [ROWS|RANGE BETWEEN frame_start AND frame_end]
)

-- Frame options:
-- UNBOUNDED PRECEDING: Từ đầu partition
-- CURRENT ROW: Row hiện tại
-- UNBOUNDED FOLLOWING: Đến cuối partition
-- N PRECEDING: N rows trước
-- N FOLLOWING: N rows sau

3.2 Ranking Functions

SELECT 
    EmployeeId,
    FullName,
    Department,
    Salary,
    
    -- ROW_NUMBER: Sequential, không ties
    ROW_NUMBER() OVER (PARTITION BY Department ORDER BY Salary DESC) AS RowNum,
    
    -- RANK: Ties có cùng rank, skip sau
    RANK() OVER (PARTITION BY Department ORDER BY Salary DESC) AS Rnk,
    
    -- DENSE_RANK: Ties có cùng rank, không skip
    DENSE_RANK() OVER (PARTITION BY Department ORDER BY Salary DESC) AS DenseRnk,
    
    -- NTILE: Chia thành N buckets đều nhau
    NTILE(4) OVER (PARTITION BY Department ORDER BY Salary DESC) AS Quartile
FROM Employees
ORDER BY Department, Salary DESC;

/*
Kết quả ví dụ (cùng Department, Salary: 10000, 8000, 8000, 6000):
FullName  Salary  RowNum  Rnk  DenseRnk  Quartile
Alice     10000   1       1    1         1
Bob       8000    2       2    2         2
Carol     8000    3       2    2         2
David     6000    4       4    3         3
*/

3.3 Offset Functions

SELECT
    OrderDate,
    Revenue,
    
    -- LAG: Giá trị từ row TRƯỚC đó
    LAG(Revenue, 1, 0) OVER (ORDER BY OrderDate) AS PrevRevenue,
    LAG(Revenue, 2, 0) OVER (ORDER BY OrderDate) AS TwoPeriodsAgo,
    
    -- LEAD: Giá trị từ row SAU đó
    LEAD(Revenue, 1, 0) OVER (ORDER BY OrderDate) AS NextRevenue,
    
    -- FIRST_VALUE: Giá trị đầu tiên trong partition/window
    FIRST_VALUE(Revenue) OVER (PARTITION BY YEAR(OrderDate) ORDER BY OrderDate
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS FirstRevenueOfYear,
    
    -- LAST_VALUE: Giá trị cuối cùng (cần explicit frame!)
    LAST_VALUE(Revenue) OVER (PARTITION BY YEAR(OrderDate) ORDER BY OrderDate
        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS LastRevenueOfYear,
    
    -- Month-over-Month Growth %
    CASE 
        WHEN LAG(Revenue) OVER (ORDER BY OrderDate) = 0 THEN NULL
        ELSE ROUND(
            (Revenue - LAG(Revenue) OVER (ORDER BY OrderDate)) * 100.0 
            / LAG(Revenue) OVER (ORDER BY OrderDate), 
            2)
    END AS MoMGrowthPct

FROM MonthlySales
ORDER BY OrderDate;

3.4 Aggregate Window Functions

-- RUNNING TOTAL (Cumulative Sum)
SELECT
    OrderDate,
    OrderAmount,
    
    -- Running total toàn bộ
    SUM(OrderAmount) OVER (
        ORDER BY OrderDate
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS RunningTotal,
    
    -- Running total theo năm (PARTITION BY)
    SUM(OrderAmount) OVER (
        PARTITION BY YEAR(OrderDate)
        ORDER BY OrderDate
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS YearlyRunningTotal,
    
    -- Moving Average: trung bình 7 ngày gần nhất
    AVG(OrderAmount) OVER (
        ORDER BY OrderDate
        ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
    ) AS MovingAvg7Day,
    
    -- Running Count
    COUNT(*) OVER (
        PARTITION BY CustomerId
        ORDER BY OrderDate
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS CustomerOrderNumber,

    -- Percent of total
    ROUND(
        OrderAmount * 100.0 / SUM(OrderAmount) OVER (),
        2
    ) AS PctOfTotal,
    
    -- Percent of year total
    ROUND(
        OrderAmount * 100.0 / SUM(OrderAmount) OVER (PARTITION BY YEAR(OrderDate)),
        2
    ) AS PctOfYearTotal

FROM Orders
ORDER BY OrderDate;

3.5 Ví Dụ Thực Tế

Pagination với ROW_NUMBER()

DECLARE @Page INT = 2;
DECLARE @PageSize INT = 10;

WITH PagedOrders AS (
    SELECT 
        OrderId,
        OrderDate,
        TotalAmount,
        CustomerName,
        ROW_NUMBER() OVER (ORDER BY OrderDate DESC, OrderId DESC) AS RowNum
    FROM Orders o
    JOIN Customers c ON o.CustomerId = c.CustomerId
    WHERE o.IsDeleted = 0
)
SELECT OrderId, OrderDate, TotalAmount, CustomerName
FROM PagedOrders
WHERE RowNum BETWEEN (@Page - 1) * @PageSize + 1 AND @Page * @PageSize;

-- Modern approach: OFFSET/FETCH (SQL 2012+)
SELECT OrderId, OrderDate, TotalAmount
FROM Orders
ORDER BY OrderDate DESC, OrderId DESC
OFFSET (@Page - 1) * @PageSize ROWS
FETCH NEXT @PageSize ROWS ONLY;

Tìm và Xóa Duplicates

-- Xem duplicates
WITH DuplicateDetection AS (
    SELECT 
        *,
        ROW_NUMBER() OVER (
            PARTITION BY Email, FirstName, LastName -- Cột xác định duplicate
            ORDER BY CustomerId ASC -- Keep: record có ID nhỏ nhất
        ) AS RowNum,
        COUNT(*) OVER (PARTITION BY Email, FirstName, LastName) AS DupeCount
    FROM Customers
)
SELECT * FROM DuplicateDetection WHERE DupeCount > 1;

-- Xóa duplicates (giữ record đầu tiên)
WITH DuplicateDetection AS (
    SELECT 
        CustomerId,
        ROW_NUMBER() OVER (
            PARTITION BY Email, FirstName, LastName
            ORDER BY CustomerId ASC
        ) AS RowNum
    FROM Customers
)
DELETE FROM DuplicateDetection WHERE RowNum > 1;

Running Total với ROWS vs RANGE

-- ROWS: Tính theo vị trí vật lý (accurate)
SELECT 
    SaleDate, Amount,
    SUM(Amount) OVER (ORDER BY SaleDate ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS RunningTotal_ROWS
FROM Sales;

-- RANGE: Tính theo giá trị, bao gồm cả ties (có thể không chính xác với duplicates)
SELECT 
    SaleDate, Amount,
    SUM(Amount) OVER (ORDER BY SaleDate RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS RunningTotal_RANGE
FROM Sales;

-- Khi có 2 rows cùng SaleDate:
-- ROWS: Row 1 là subtotal đến row 1, Row 2 là subtotal đến row 2
-- RANGE: Cả hai rows đều hiển thị cùng tổng (tổng đến cuối ngày đó)
-- ROWS nhanh hơn và thường là điều bạn muốn

Gaps and Islands

-- Tìm các dải ngày liên tiếp user login (Islands)
WITH LoginDates AS (
    SELECT DISTINCT UserId, CAST(LoginDateTime AS DATE) AS LoginDate
    FROM UserLogins
),
Ranked AS (
    SELECT 
        UserId, LoginDate,
        ROW_NUMBER() OVER (PARTITION BY UserId ORDER BY LoginDate) AS RowNum
    FROM LoginDates
),
Islands AS (
    SELECT
        UserId,
        LoginDate,
        DATEADD(DAY, -RowNum, LoginDate) AS IslandKey -- Ngày - RowNum = hằng số khi liên tiếp
    FROM Ranked
)
SELECT
    UserId,
    MIN(LoginDate) AS StreakStart,
    MAX(LoginDate) AS StreakEnd,
    COUNT(*) AS ConsecutiveDays,
    DATEDIFF(DAY, MIN(LoginDate), MAX(LoginDate)) + 1 AS TotalDays
FROM Islands
GROUP BY UserId, IslandKey
HAVING COUNT(*) >= 3 -- Chỉ những streak >= 3 ngày
ORDER BY UserId, StreakStart;

-- Tìm Gaps (khoảng trống)
WITH AllDates AS (
    SELECT 
        UserId, LoginDate,
        LEAD(LoginDate) OVER (PARTITION BY UserId ORDER BY LoginDate) AS NextLogin
    FROM (
        SELECT DISTINCT UserId, CAST(LoginDateTime AS DATE) AS LoginDate
        FROM UserLogins
    ) d
)
SELECT 
    UserId, 
    LoginDate AS GapStart,
    NextLogin AS GapEnd,
    DATEDIFF(DAY, LoginDate, NextLogin) - 1 AS GapDays
FROM AllDates
WHERE DATEDIFF(DAY, LoginDate, NextLogin) > 1; -- Gap lớn hơn 1 ngày

PHẦN 4: Cursors

4.1 Cú Pháp Cursor

-- Cú pháp đầy đủ
DECLARE cursor_name CURSOR
    [LOCAL | GLOBAL]
    [FORWARD_ONLY | SCROLL]
    [STATIC | KEYSET | DYNAMIC | FAST_FORWARD]
    [READ_ONLY | SCROLL_LOCKS | OPTIMISTIC]
FOR
    select_statement
[FOR UPDATE [OF column_name [,...n]]];

OPEN cursor_name;

FETCH [NEXT | PRIOR | FIRST | LAST | ABSOLUTE n | RELATIVE n] 
FROM cursor_name INTO @var1 [, @var2 ...];

WHILE @@FETCH_STATUS = 0
BEGIN
    -- Process...
    FETCH NEXT FROM cursor_name INTO @var1;
END;

CLOSE cursor_name;
DEALLOCATE cursor_name;

4.2 Cursor Types

TypeMô tảUse Case
FAST_FORWARDRead-only, forward-only, nhanh nhấtKhi chỉ cần đi qua một lần
STATICSnapshot data vào tempdb, data cố địnhKhi data có thể thay đổi trong khi đọc
KEYSETKeys được lưu, data rows được đọc lạiBiến đổi data visible, insert không
DYNAMICPhản ánh mọi thay đổi real-timeKhi cần thấy live changes
-- FAST_FORWARD: Nhanh nhất, chỉ read-only, forward
DECLARE @OrderId INT;
DECLARE @Amount DECIMAL(18,2);

DECLARE cur_Orders CURSOR FAST_FORWARD FOR
    SELECT OrderId, TotalAmount
    FROM Orders
    WHERE Status = 'Pending'
    ORDER BY OrderId;

OPEN cur_Orders;
FETCH NEXT FROM cur_Orders INTO @OrderId, @Amount;

WHILE @@FETCH_STATUS = 0
BEGIN
    -- Gọi SP cho từng order (lý do duy nhất dùng cursor)
    EXEC dbo.ProcessPendingOrder @OrderId, @Amount;
    
    FETCH NEXT FROM cur_Orders INTO @OrderId, @Amount;
END;

CLOSE cur_Orders;
DEALLOCATE cur_Orders;

4.3 Tại Sao Tránh Cursors

-- BENCHMARK: Cursor vs Set-based

-- Cursor approach: O(n) round trips
DECLARE @Id INT;
DECLARE cur CURSOR FOR SELECT ProductId FROM Products;
OPEN cur;
FETCH NEXT FROM cur INTO @Id;
WHILE @@FETCH_STATUS = 0
BEGIN
    UPDATE Products SET Stock = Stock - 1 WHERE ProductId = @Id; -- 1 update mỗi lần
    FETCH NEXT FROM cur INTO @Id;
END;
CLOSE cur; DEALLOCATE cur;
-- Với 100,000 products: 100,000 update statements, 100,000 lock acquisitions

-- Set-based approach: O(1) - một câu lệnh
UPDATE Products SET Stock = Stock - 1; -- 1 update cho tất cả
-- Với 100,000 products: 1 operation, optimized I/O, parallel execution

4.4 Alternatives to Cursors

-- THAY CURSOR BẰNG:

-- 1. Set-based operations
-- Cursor: update từng row
-- Better: UPDATE Table SET col = expr WHERE condition

-- 2. Window Functions
-- Cursor: dùng biến để track previous row
-- Better: LAG(), ROW_NUMBER(), SUM() OVER()

-- 3. Recursive CTE
-- Cursor: duyệt hierarchy từng node
-- Better: Recursive CTE

-- 4. APPLY (cho SP/function per row)
-- Cursor: EXEC SP cho từng row
-- Nếu SP có thể convert thành TVF, dùng CROSS APPLY

-- 5. Batch processing (nếu phải xử lý từng batch)
DECLARE @BatchSize INT = 1000;
DECLARE @LastId INT = 0;

WHILE 1 = 1
BEGIN
    UPDATE TOP (@BatchSize) p
    SET p.ProcessedFlag = 1
    OUTPUT inserted.ProductId
    FROM Products p
    WHERE p.ProcessedFlag = 0 AND p.ProductId > @LastId
    ORDER BY p.ProductId;

    IF @@ROWCOUNT = 0 BREAK;
    
    SET @LastId = @LastId + @BatchSize;
    WAITFOR DELAY '00:00:01'; -- Giảm tải
END;

-- 6. STRING_AGG thay vì cursor concatenate strings
-- Cursor approach: dùng biến + concatenation
-- Better:
SELECT 
    DepartmentId,
    STRING_AGG(FullName, ', ') WITHIN GROUP (ORDER BY FullName) AS EmployeeList
FROM Employees
GROUP BY DepartmentId;

Tóm Tắt So Sánh

Khi nào dùng gì?

Kỹ thuậtDùng khiTránh khi
ViewSimplify complex queries, security layerCần parameters (dùng TVF thay)
Indexed ViewPre-aggregate large fact tablesBảng hay INSERT/UPDATE (overhead cao)
CTEReadable queries, recursive, DELETE/UPDATEDataset lớn cần index (dùng temp table)
Window FunctionRanking, running totals, lag/leadKhông có ORDER BY trong OVER() (cho một số functions)
CursorGọi SP từng row, không thể set-basedMọi trường hợp khác

Performance Quick Reference

-- Checking view dependencies
SELECT 
    referencing_entity_name,
    referenced_entity_name
FROM sys.dm_sql_referencing_entities('dbo.Orders', 'OBJECT');

-- Find expensive window function queries
SELECT TOP 10
    qs.total_elapsed_time / qs.execution_count AS avg_elapsed_time,
    qs.execution_count,
    SUBSTRING(qt.text, 1, 200) AS query_snippet
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
WHERE qt.text LIKE '%OVER%'
ORDER BY avg_elapsed_time DESC;