Hướng Dẫn Sử Dụng CTE Trong SQL Để Tổ Chức Truy Vấn Nhiều Bước

Hướng dẫn sử dụng CTE trong SQL để tổ chức truy vấn nhiều bước

Khi xử lý các yêu cầu trích xuất dữ liệu kinh doanh phức tạp, hầu như mọi Data Analyst đều từng rơi vào cái bẫy của truy vấn lồng nhau (Nested Subqueries). Khi một câu lệnh SQL có tới 3-4 tầng ngoặc đơn lồng nhau, việc đọc hiểu luồng dữ liệu, gỡ lỗi (debug) hay bàn giao mã nguồn cho đồng nghiệp trở thành một thách thức lớn. CTE (Common Table Expression) xuất hiện như một giải pháp chuẩn mực giúp chuẩn hóa tư duy truy vấn: biến mã nguồn từ dạng “xoắn ốc lồng ghép” thành luồng dữ liệu tuyến tính từ trên xuống dưới.

Bài viết này cung cấp một hướng dẫn thực chiến toàn diện: từ bản chất thực thi của CTE trong Database Engine, phân tích so sánh với Subquery và Bảng tạm, cho đến cách tổ chức nhiều CTE (Multiple CTEs) để giải quyết bài toán đo lường doanh thu đơn hàng thứ hai (Repeat Purchase Revenue).

💡 TƯ DUY TỔ CHỨC TRUY VẤN VỚI CTE CHUẨN MỰC
CTE không sinh ra để làm phức tạp hóa câu lệnh SQL. Mục đích cốt lõi của CTE là chia tách bài toán lớn thành các bước xử lý dữ liệu độc lập, minh bạch và có thứ tự logic rõ ràng. Nhờ đó, người viết dễ kiểm soát logic nghiệp vụ và đồng nghiệp tiếp nhận mã nguồn có thể đọc hiểu ngay lập tức.

Theo tài liệu kỹ thuật chính thức từ Microsoft Learn (T-SQL Reference), CTE (Common Table Expression) là một tập kết quả tạm thời được đặt tên (temporary named result set), được phát sinh từ một câu truy vấn đơn giản và được định nghĩa trong phạm vi thực thi của một câu lệnh duy nhất như SELECT, INSERT, UPDATE, DELETE hoặc MERGE.

💡 Điểm cốt lõi Data Analyst cần ghi nhớ: CTE không phải là một bảng vật lý trên ổ cứng. Về mặt kiến trúc, CTE hoạt động tương tự như một Inline View (khung nhìn nội tuyến) chỉ tồn tại trong suốt quá trình câu truy vấn chính thực thi.

Ba đặc tính cốt lõi tạo nên sức mạnh của CTE:

  • Phạm vi thực thi khép kín (Scope): CTE chỉ khả dụng ngay trong câu lệnh theo sau nó. Ngay khi câu lệnh chính kết thúc, kết quả tạm thời này sẽ tự động được giải phóng khỏi ngữ cảnh thực thi.
  • Tái sử dụng nội bộ (Internal Reusability): Trong phạm vi của câu lệnh chính, bạn có thể gọi tên CTE nhiều lần (ví dụ: tự JOIN với chính nó) mà không cần phải viết lại toàn bộ khối mã nguồn.
  • Khả năng đệ quy (Recursive CTE): CTE có thể tự tham chiếu lại chính nó, hỗ trợ giải quyết các bài toán dữ liệu dạng phân cấp (cây danh mục sản phẩm, cơ cấu tổ chức quản lý) mà các câu lệnh SQL thông thường khó xử lý.

2. Ma trận so sánh: CTE vs Subquery vs Bảng tạm (#Temp Table)

Một câu hỏi thường gặp khi đi phỏng vấn vị trí Data Analyst là: “Khi nào nên dùng CTE, khi nào nên dùng Subquery và khi nào phải dùng Bảng tạm?”. Dưới đây là bảng đối chiếu kỹ thuật chuẩn mực:

Tiêu chí so sánh Subquery (Truy vấn con) CTE (WITH … AS) Bảng tạm (#Temp Table)
Khả năng đọc hiểu Rất khó đọc khi lồng > 2 cấp Tối ưu nhất (Tuyến tính từ trên xuống) Rõ ràng theo từng khối lệnh
Vị trí lưu trữ Bộ nhớ tạm của Optimizer Bộ nhớ tạm của Optimizer (Inline) Lưu vật lý trong cơ sở dữ liệu tempdb
Khả năng tạo Index Không hỗ trợ Không hỗ trợ Hỗ trợ tạo Clustered/Non-clustered Index
Phạm vi sử dụng Cục bộ trong câu lệnh Cục bộ trong 1 câu lệnh duy nhất Toàn bộ phiên làm việc (Session)
Kịch bản tối ưu Lọc giá trị nhanh trong WHERE (SELECT MAX...) Tổ chức phân tích nghiệp vụ 2-4 bước Dữ liệu lớn (> 1 triệu dòng), tính toán phức tạp

3. Cú pháp chuẩn T-SQL: Từ CTE đơn đến Multiple CTEs

a. Cú pháp CTE đơn (Single CTE)

Một CTE cơ bản luôn mở đầu bằng từ khóa WITH, theo sau là tên đặt cho CTE và câu lệnh truy vấn nằm trong cặp dấu ngoặc đơn (...):

●●● SQL (T-SQL) – Single CTE
-- Dấu chấm phẩy ngăn chặn lỗi batch của T-SQL
;WITH cte_active_customers AS (
    SELECT customer_id, customer_name, city
    FROM customers
    WHERE status = 'Active'
)
-- Truy vấn chính gọi CTE ngay bên dưới
SELECT city, COUNT(customer_id) AS total_customers
FROM cte_active_customers
GROUP BY city;

b. Cú pháp nhiều CTE nối tiếp (Multiple CTEs)

Đây là kỹ thuật mạnh mẽ nhất của CTE: bạn có thể định nghĩa liên tiếp nhiều khối xử lý. Quy tắc quan trọng cần nhớ: chỉ sử dụng duy nhất một từ khóa WITH ở đầu, các CTE tiếp theo phân tách nhau bằng dấu phẩy ,. Đặc biệt, CTE đứng sau hoàn toàn có thể tham chiếu trực tiếp đến dữ liệu của các CTE đã định nghĩa phía trước:

●●● SQL (T-SQL) – Multiple CTEs
;WITH cte_step_one AS (
    SELECT ...
),
cte_step_two AS (
    -- Tham chiếu đến kết quả của cte_step_one
    SELECT ...
    FROM cte_step_one
)
SELECT * 
FROM cte_step_two;

4. Bài toán thực chiến: Phân tích doanh thu đơn hàng thứ hai (Repeat Purchase)

4.1 Bối cảnh kinh doanh & Schema dữ liệu mẫu

Trong ngành Bán lẻ và Thương mại điện tử (E-commerce), đơn hàng thứ 2 là cột mốc sống còn (inflection point). Đây là thời điểm phân định một người dùng là “khách mua thử một lần rồi rời bỏ” hay đã chuyển hóa thành “khách hàng quay lại (Repeat Customer)”.

🎯 Yêu cầu từ Giám đốc Kinh doanh:
“Hãy phân tích theo từng thành phố: Có bao nhiêu khách hàng đã phát sinh đơn hàng thứ 2 hợp lệ (đã thanh toán)? Giá trị doanh thu trung bình của đơn hàng lần thứ 2 tại mỗi thành phố là bao nhiêu?”

Giả sử chúng ta có 2 bảng dữ liệu nguồn với cấu trúc mẫu như sau:

Bảng orders (Lịch sử giao dịch):

order_id customer_id order_date total_amount order_status
ORD-101 CUST-01 2026-01-10 500,000 Completed
ORD-102 CUST-01 2026-02-15 750,000 Completed
ORD-103 CUST-02 2026-01-20 300,000 Completed
ORD-104 CUST-02 2026-03-01 450,000 Completed
ORD-105 CUST-03 2026-02-05 600,000 Cancelled

Bảng customers (Hồ sơ khách hàng):

customer_id customer_name city
CUST-01 Nguyễn Văn An Hà Nội
CUST-02 Trần Thị Bình TP. Hồ Chí Minh
CUST-03 Lê Minh Cường Đà Nẵng

4.2 Viết truy vấn T-SQL với cấu trúc Multiple CTEs

Toàn bộ câu lệnh SQL được chia thành 2 CTE nối tiếp nhau trước khi tổng hợp ở truy vấn chính:

●●● SQL Server – Case Study: Repeat Purchase
-- BƯỚC 1: Xếp hạng thứ tự đơn hàng của từng khách hàng
;WITH cte_ranked_orders AS (
    SELECT 
        order_id,
        customer_id,
        order_date,
        total_amount,
        ROW_NUMBER() OVER (
            PARTITION BY customer_id 
            ORDER BY order_date ASC
        ) AS order_sequence
    FROM orders
    WHERE order_status = 'Completed'
),

-- BƯỚC 2: Trích xuất chính xác các đơn hàng ở lần mua thứ hai
cte_second_orders AS (
    SELECT 
        customer_id,
        order_id,
        total_amount AS second_order_value
    FROM cte_ranked_orders
    WHERE order_sequence = 2
)

-- BƯỚC 3: Ghép nối bảng khách hàng để tổng hợp báo cáo theo thành phố
SELECT 
    c.city AS customer_city,
    COUNT(s.customer_id) AS repeat_customer_count,
    ROUND(AVG(s.second_order_value), 0) AS avg_second_order_revenue
FROM cte_second_orders s
INNER JOIN customers c ON s.customer_id = c.customer_id
GROUP BY c.city
ORDER BY avg_second_order_revenue DESC;

4.3 Bóc tách luồng dữ liệu từng bước và bảng kết quả

Cơ chế xử lý diễn ra như thế nào qua từng khối lệnh?

  • Khối cte_ranked_orders: Hàm Window Function ROW_NUMBER() chia cụm dữ liệu theo từng customer_id (nhờ PARTITION BY) và sắp xếp ngày mua tăng dần. Kết quả sinh ra cột order_sequence (1 cho đơn đầu, 2 cho đơn tiếp theo…). Đồng thời, điều kiện WHERE order_status = 'Completed' loại trừ ngay các đơn hàng bị hủy trước khi tính thứ hạng.
  • Khối cte_second_orders: Kế thừa tập dữ liệu đã đánh số từ bước 1 và chỉ lọc đúng điều kiện order_sequence = 2. Lúc này, chỉ những khách hàng thực sự quay lại mua lần 2 mới được giữ lại.
  • Khối SELECT cuối cùng: Thực hiện INNER JOIN với bảng danh mục customers để lấy chiều thông tin địa lý (city), áp dụng hàm tổng hợp COUNT() đếm số lượng khách mua lại và AVG() tính doanh thu trung bình.

Kết quả báo cáo đầu ra (Final Query Result):

customer_city repeat_customer_count avg_second_order_revenue (VND)
Hà Nội 1 750,000
TP. Hồ Chí Minh 1 450,000

5. Best Practices & 5 Lỗi thường gặp cần tránh

❌ Lỗi 1: Thiếu dấu chấm phẩy (;) phân tách câu lệnh trước WITH
🚨 Triệu chứng: Báo lỗi cú pháp T-SQL: “Incorrect syntax near the keyword ‘with’. If this statement is a common table expression, an xmlnamespaces clause or a change tracking context clause, the previous statement must be terminated with a semicolon.”
🔍 Nguyên nhân: Trình biên dịch SQL Server yêu cầu câu lệnh trước mệnh đề WITH bắt buộc phải được đóng lại bằng dấu chấm phẩy.
🔧 Cách xử lý: Luôn chủ động đặt dấu chấm phẩy ngay trước từ khóa: ;WITH cte_name AS (...).
❌ Lỗi 2: Cố tình gọi lại CTE sau khi câu lệnh chính đã kết thúc
🚨 Triệu chứng: Báo lỗi “Invalid object name ‘cte_name'” ở câu truy vấn thứ hai.
🔍 Nguyên nhân: CTE có phạm vi (scope) cực kỳ nghiêm ngặt: chỉ tồn tại cho duy nhất một câu lệnh thực thi tiếp theo.
🔧 Cách xử lý: Nếu bạn cần tái sử dụng kết quả trung gian cho nhiều câu lệnh độc lập trong phiên làm việc, hãy chuyển sang dùng bảng tạm #Temp_Table.
❌ Lỗi 3: Trùng tên cột hoặc thiếu Alias trong định nghĩa CTE
🚨 Triệu chứng: “The column ‘…’ was specified multiple times for ‘cte_name'”.
🔍 Nguyên nhân: Xảy ra khi SELECT các hàm tổng hợp (như COUNT(*), SUM()) mà quên đặt alias, hoặc JOIN 2 bảng có chung tên cột (ví dụ cùng có cột id).
🔧 Cách xử lý: Đảm bảo mọi cột đầu ra trong CTE đều có tên định danh duy nhất (Unique column names).
❌ Lỗi 4: Lạm dụng CTE trên tập dữ liệu hàng chục triệu dòng (Performance Trap)
🚨 Triệu chứng: Câu truy vấn chạy chậm bất thường, chiếm dụng tài nguyên CPU cao.
🔍 Nguyên nhân: Trong SQL Server, kết quả CTE không được ghi nhớ sẵn (no materialization). Nếu bạn gọi CTE 3 lần trong câu truy vấn chính, hệ thống sẽ thực thi lại câu lệnh định nghĩa CTE đó 3 lần.
🔧 Cách xử lý: Với các bảng dữ liệu lịch sử cực lớn, hãy sử dụng SELECT ... INTO #TempTable và tạo Clustered Index trên cột khóa chính để tối ưu hóa thời gian quét.
❌ Lỗi 5: Vòng lặp đệ quy vô tận trong Recursive CTE
🚨 Triệu chứng: “The statement terminated. The maximum recursion 100 has been exhausted before statement completion.”
🔍 Nguyên nhân: Điều kiện dừng (Termination Condition) trong mệnh đề WHERE của phần đệ quy bị viết sai hoặc cấu trúc dữ liệu có chu trình khép kín (A quản lý B, B quản lý A).
🔧 Cách xử lý: Rà soát kỹ điều kiện dừng, hoặc sử dụng gợi ý OPTION (MAXRECURSION n) ở cuối câu lệnh để giới hạn số lần lặp an toàn.

6. Tài liệu tham khảo chính thống

ĐỒNG HÀNH CÙNG MCNA TECHNOLOGY SCHOOL
Khám Phá Lộ Trình Học Tập Chuyên Sâu Tại MCNA Technology School
Nâng cao tư duy lập trình, xử lý dữ liệu và làm chủ công nghệ AI cùng các chương trình đào tạo chuyên sâu:
BI & AI Analytics
Làm chủ tư duy phân tích dữ liệu, tự động hóa xử lý và xây dựng báo cáo quản trị thông minh với Power BI và AI.
Python Data & Automation
Từ nền tảng lập trình đến phân tích dữ liệu thực chiến, xây dựng data pipeline và ứng dụng học máy vào kinh doanh.
Tác giả: Nguyễn Vũ Tiến Dũng – Data Analyst tại MCNA Technology School
🌐 Website: mcna.vn
📞 Hotline: 0939.866.825
📍 Hà Nội: 30 Trung Liệt, Đống Đa | Liền kề 44B TT2 Văn Quán, Hà Đông
📍 TP. Hồ Chí Minh: 50B Phan Tây Hồ, Phường Cầu Kiệu, Phú Nhuận
→ Chỉ mục