0

Trino SQL Bài học 18: Lọc kết quả sau khi gom nhóm với HAVING

Mục tiêu bài học:

  • Phân biệt rạch ròi giới hạn của WHERE và sức mạnh của HAVING.

  • Hiểu sâu sắc thứ tự thực thi trong pipeline xử lý câu lệnh SQL.

  • Tối ưu hóa hiệu năng bằng cách kết hợp nhịp nhàng giữa lọc dòng (Row-level) và lọc nhóm (Group-level).

1. Tại sao lại cần HAVING khi đã có WHERE?

Một lỗi kinh điển mà những người mới viết SQL thường mắc phải là cố gắng lọc kết quả của một hàm tổng hợp ngay trong mệnh đề WHERE.

Giả sử bạn muốn tìm các trạng thái đơn hàng mang lại tổng doanh thu lớn hơn 500,000. Bạn viết:

SQL

-- CÚ PHÁP SAI (Trino sẽ báo lỗi: Aggregate functions are not allowed in WHERE)
SELECT 
    orderstatus, 
    SUM(totalprice) AS revenue
FROM tpch.tiny.orders
WHERE SUM(totalprice) > 500000 
GROUP BY orderstatus;

Nguyên nhân lỗi: Nhớ lại thứ tự thực thi ở Bài 17: WHERE chạy trước GROUP BY. Tại thời điểm WHERE hoạt động, dữ liệu chưa hề được gom nhóm, Trino không thể biết tổng doanh thu (SUM) của từng trạng thái là bao nhiêu để mà lọc.

Mệnh đề HAVING ra đời để giải quyết bài toán này. Nó đóng vai trò là "màng lọc thứ hai", hoạt động sau khi GROUP BY đã tính toán xong.

2. Cú pháp cơ bản của HAVING

Mệnh đề HAVING phải luôn đứng sau GROUP BY và đứng trước ORDER BY. Nó cho phép bạn sử dụng trực tiếp các hàm tổng hợp (SUM, COUNT, AVG,...) làm điều kiện lọc.

SQL

-- CÚ PHÁP ĐÚNG
SELECT 
    orderstatus, 
    SUM(totalprice) AS revenue
FROM tpch.tiny.orders
GROUP BY orderstatus
HAVING SUM(totalprice) > 500000; -- Lọc các nhóm có doanh thu thỏa mãn điều kiện

Bạn cũng có thể lọc dựa trên bí danh (alias) của cột đã được khai báo ở mệnh đề SELECT, tuy nhiên việc viết rõ hàm tổng hợp trong HAVING là cách viết chuẩn mực và an toàn nhất trên mọi hệ thống CSDL.

3. Sự kết hợp hoàn hảo: Lọc hai tầng (WHERE + HAVING)

Trong các bài toán phân tích thực tế, bạn gần như luôn phải sử dụng cả hai mệnh đề này trong cùng một câu lệnh.

  • Tầng 1 (WHERE): Lọc bỏ các dòng dữ liệu rác hoặc không nằm trong phạm vi thời gian phân tích trước khi tính toán.

  • Tầng 2 (HAVING): Lọc bỏ các nhóm không đạt KPI sau khi đã tính toán.

Ví dụ: Tìm những phân khúc thị trường có tổng giá trị đơn hàng khẩn cấp (URGENT) trong năm 1995 vượt mức 1 triệu.

SQL

SELECT 
    c.mktsegment,
    SUM(o.totalprice) AS urgent_revenue,
    COUNT(o.orderkey) AS total_orders
FROM tpch.tiny.orders AS o
JOIN tpch.tiny.customer AS c ON o.custkey = c.custkey
WHERE 
    o.orderpriority = '1-URGENT'                     -- (1) Lọc dòng: Chỉ lấy đơn khẩn
    AND o.orderdate BETWEEN DATE '1995-01-01' AND DATE '1995-12-31' -- Lọc dòng: Chỉ lấy năm 1995
GROUP BY 
    c.mktsegment                                     -- (2) Gom nhóm theo phân khúc
HAVING 
    SUM(o.totalprice) > 1000000;                     -- (3) Lọc nhóm: Chỉ lấy nhóm có doanh thu > 1 triệu

4. Bức tranh toàn cảnh: Thứ tự thực thi cập nhật

Đến bài học này, bạn đã sở hữu gần như đầy đủ các khối block cốt lõi của một câu lệnh SQL phân tích hoàn chỉnh. Hãy khắc sâu thứ tự thực thi (Evaluation Order) này để không bao giờ bị nhầm lẫn logic:

  1. FROM / JOIN: Quét và kết nối dữ liệu từ các Data Source.

  2. WHERE: Loại bỏ các dòng (Row) không cần thiết ở từng Worker.

  3. GROUP BY: Đóng gói các dòng còn lại thành từng Thùng (Bucket).

  4. Hàm tổng hợp: Tính toán số liệu (SUM, COUNT) cho từng Thùng.

  5. HAVING: Vứt bỏ các Thùng không đạt chuẩn.

  6. SELECT: Chiết xuất các thông tin cần hiển thị lên màn hình.

  7. ORDER BY: Sắp xếp thứ tự các Thùng hiển thị.

  8. LIMIT: Trả về số lượng Thùng giới hạn.

5. Góc nhìn Kỹ sư: Tối ưu I/O và Memory

Trong hệ thống phân tán của Trino, đẩy điều kiện lọc lên càng sớm càng tốt là chìa khóa để giữ cho hệ thống không bị quá tải.

Luật tối ưu hóa: Nếu một điều kiện có thể dùng được ở WHERE, tuyệt đối không mang nó xuống HAVING.

Ví dụ về cách viết tồi (Anti-pattern):

SQL

-- CÚ PHÁP TỒI: Chạy chậm và ngốn RAM
SELECT 
    orderstatus, 
    SUM(totalprice)
FROM tpch.tiny.orders
GROUP BY orderstatus
HAVING orderstatus = 'O'; 
-- orderstatus không phải là hàm tổng hợp, đáng lẽ phải bị lọc từ vòng gửi xe!

Hệ quả của cách viết trên là Trino sẽ bắt các Worker phải đọc và tính tổng doanh thu cho tất cả các trạng thái (O, F, P), truyền dữ liệu qua mạng về Coordinator, rồi Coordinator mới vứt bỏ F và P đi. Nếu dùng WHERE orderstatus = 'O', các Worker sẽ chỉ đọc và tính toán riêng dữ liệu của 'O', giúp tiết kiệm 2/3 tài nguyên hệ thống.


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.