CodeGym /Các khóa học /SQL SELF /CTE vs truy vấn con: nên chọn cái nào?

CTE vs truy vấn con: nên chọn cái nào?

SQL SELF
Mức độ , Bài học
Có sẵn

Tụi mình biết rồi là CTE làm code dễ đọc hơn hẳn. Nhưng có phải lúc nào cũng nên dùng không? Đôi khi truy vấn con đơn giản lại chạy nhanh và gọn hơn. Cùng xem khi nào mỗi cái phát huy tác dụng, và học cách chọn cho hợp lý nhé.

Truy vấn con: nhanh và đơn giản

Bạn nhớ rồi đúng không, truy vấn con là SQL lồng trong SQL. Nó nhét thẳng vào truy vấn chính và chạy "tại chỗ" luôn. Quá hợp cho mấy thao tác đơn giản, dùng một lần:

-- Tìm sản phẩm có giá cao hơn giá trung bình
SELECT product_name, price
FROM products
WHERE price > (SELECT AVG(price) FROM products);

Ở đây truy vấn con tính giá trung bình một lần là xong. Không cần cấu trúc gì phức tạp cả.

Hiệu năng: ai nhanh hơn?

Truy vấn con thường thắng về tốc độ với thao tác đơn giản. PostgreSQL tối ưu được "tại chỗ", nhất là khi truy vấn con chỉ chạy một lần:

-- Nhanh: truy vấn con chỉ chạy một lần
SELECT customer_id, order_total
FROM orders
WHERE order_date = (SELECT MAX(order_date) FROM orders);

CTE mặc định sẽ được materialize — PostgreSQL sẽ tính kết quả CTE trước, lưu thành bảng tạm rồi mới dùng. Cái này có thể làm chậm truy vấn đơn giản:

-- Chậm hơn: CTE sẽ thành bảng tạm
WITH latest_date AS (
    SELECT MAX(order_date) AS max_date FROM orders
)
SELECT customer_id, order_total
FROM orders, latest_date
WHERE order_date = max_date;

Nhưng mà! Từ PostgreSQL 12 trở đi bạn có thể kiểm soát việc materialize:

-- Ép KHÔNG materialize
WITH latest_date AS NOT MATERIALIZED (
    SELECT MAX(order_date) AS max_date FROM orders
)
SELECT customer_id, order_total
FROM orders, latest_date
WHERE order_date = max_date;

Dùng lại nhiều lần: CTE là vua

Khi bạn cần dùng lại cùng một kết quả trung gian nhiều lần, CTE là không thể thiếu luôn:

-- Với truy vấn con: lặp lại logic hai lần
SELECT
    (SELECT COUNT(*) FROM orders WHERE status = 'completed') AS completed_orders,
    (SELECT COUNT(*) FROM orders WHERE status = 'completed') * 100.0 / COUNT(*) AS completion_rate
FROM orders;

-- Với CTE: tính một lần, dùng hai lần
WITH completed_orders AS (
    SELECT COUNT(*) AS count FROM orders WHERE status = 'completed'
)
SELECT
    co.count AS completed_orders,
    co.count * 100.0 / (SELECT COUNT(*) FROM orders) AS completion_rate
FROM completed_orders co;

Phân tích phức tạp: CTE thắng áp đảo

Với phân tích nhiều bước, CTE biến mớ hỗn độn thành trật tự. So sánh báo cáo doanh số này nhé:

Dùng truy vấn con (rối não):

SELECT 
    category,
    revenue,
    revenue * 100.0 / (
        SELECT SUM(p.price * oi.quantity)
        FROM order_items oi
        JOIN products p ON oi.product_id = p.product_id
        JOIN orders o ON oi.order_id = o.order_id
        WHERE EXTRACT(year FROM o.order_date) = 2024
    ) AS revenue_share
FROM (
    SELECT 
        p.category,
        SUM(p.price * oi.quantity) AS revenue
    FROM order_items oi
    JOIN products p ON oi.product_id = p.product_id
    JOIN orders o ON oi.order_id = o.order_id
    WHERE EXTRACT(year FROM o.order_date) = 2024
    GROUP BY p.category
) category_revenue;

Dùng CTE (rõ ràng từng bước):

WITH yearly_sales AS (
    SELECT 
        p.category,
        p.price * oi.quantity AS sale_amount
    FROM order_items oi
    JOIN products p ON oi.product_id = p.product_id
    JOIN orders o ON oi.order_id = o.order_id
    WHERE EXTRACT(year FROM o.order_date) = 2024
),
category_revenue AS (
    SELECT 
        category,
        SUM(sale_amount) AS revenue
    FROM yearly_sales
    GROUP BY category
),
total_revenue AS (
    SELECT SUM(sale_amount) AS total FROM yearly_sales
)
SELECT 
    cr.category,
    cr.revenue,
    cr.revenue * 100.0 / tr.total AS revenue_share
FROM category_revenue cr, total_revenue tr;

Đệ quy: CTE độc quyền

Với cấu trúc phân cấp, truy vấn con bó tay luôn.

Chỉ có CTE đệ quy mới xử lý được kiểu bài toán "tìm tất cả nhân viên dưới quyền manager":

WITH RECURSIVE employee_hierarchy AS (
    -- Bắt đầu từ CEO
    SELECT employee_id, manager_id, name, 1 AS level
    FROM employees
    WHERE manager_id IS NULL

    UNION ALL

    -- Thêm nhân viên từng cấp
    SELECT e.employee_id, e.manager_id, e.name, eh.level + 1
    FROM employees e
    JOIN employee_hierarchy eh ON e.manager_id = eh.employee_id
)
SELECT * FROM employee_hierarchy ORDER BY level, name;

Debug và bảo trì code

CTE dễ debug từng phần luôn:

-- Kiểm tra bước đầu tiên
WITH active_customers AS (
    SELECT customer_id FROM customers WHERE status = 'active'
)
SELECT COUNT(*) FROM active_customers; -- Đảm bảo logic đúng

-- Thêm bước thứ hai
WITH active_customers AS (...),
recent_orders AS (
    SELECT customer_id, COUNT(*) as order_count
    FROM orders
    WHERE order_date >= '2024-01-01'
    GROUP BY customer_id
)
SELECT COUNT(*) FROM recent_orders; -- Kiểm tra luôn bước này

Truy vấn con khó debug hơn — phải tách ra khỏi ngữ cảnh mới test được.

Lời khuyên thực tế

Dùng truy vấn con khi:

  • Logic đơn giản, gói gọn trong một dòng
  • Cần hiệu năng tối đa cho thao tác đơn giản
  • Kết quả trung gian chỉ dùng một lần
  • Làm việc với dữ liệu nhỏ

Dùng CTE khi:

  • Truy vấn phức tạp, chia thành nhiều bước logic
  • Cần dùng lại kết quả trung gian nhiều lần
  • Code cần dễ đọc, dễ bảo trì
  • Làm việc với cấu trúc phân cấp (CTE đệ quy)
  • Debug logic phức tạp từng phần

Luật vàng

Bắt đầu với truy vấn con. Nếu thấy khó đọc hoặc logic lặp lại — chuyển sang CTE liền. Đồng nghiệp tương lai (hoặc chính bạn sau nửa năm nữa) sẽ cảm ơn bạn đó!

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION