Hành trình đếm sao – 3. Thứ tự thực hiện các mệnh đề của câu lệnh SELECT trong SQL Server

Một câu lệnh SELECT trong SQL Server thường thấy có hình dáng như sau
SELECT DISTINCT <TOP> <danh sách các cột>
FROM <tên bảng 1>
<kiểu join> JOIN <tên bảng 2>
ON <các điều kiện join>
WHERE <các điều kiện where>
GROUP BY <danh sách các cột group by>
HAVING <các điều kiện having>
ORDER BY <danh sách các cột order by>

SQL là kiểu ngôn ngữ khai báo, nghĩa là SQL Server sẽ không thực thi những dòng code theo thứ tự bạn đã viết mà thay vào đó nó sẽ viết lại theo ý nó để việc thực thi đạt hiệu quả tốt hơn. Khi bắt đầu tiếp xúc với T-SQL bạn cần làm quen một số thuật ngữ để dễ trao đổi, thảo luận với mọi người đặc biệt hơn là hiểu được ý tác tác giả đang muốn đề cập đến đối tượng nào khi đọc một quyển sách hoặc các bài viết liên quan trên mạng. Bên cạnh đó, bạn cũng cần hiểu rõ cấu trúc của câu lệnh SELECT và trình tự xử lý ở mức luận lý để viết code T-SQL linh hoạt hơn và không bị dư thừa, gây khó hiểu thậm chí ảnh hưởng tốc độ truy vấn.

Ví dụ sau sử dụng cơ sở dữ liệu stackoverflow như hướng dẫn ở bài trước. Hãy cùng xét câu truy vấn dưới đây:

SELECT location, COUNT(*) AS cnt
FROM Users u
	INNER JOIN Posts p ON u.Id = p.OwnerUserId
WHERE PostTypeId = 2
GROUP BY location
HAVING location LIKE '%vietnam%'
ORDER BY cnt

Trong cơ sở dữ liệu stackoverflow này, mỗi câu hỏi hoặc câu trả lời xem như là một bài post và sẽ có PostTypeId tương ứng là 1 cho câu hỏi và 2 cho câu trả lời. Nếu bạn thắc mắc “có bao nhiêu bài post trả lời của những người dùng ở Vietnam có location giống nhau?” thì hãy dùng câu truy vấn trên. Kết quả trả về như hình bên dưới:

câu lệnh select trong SQL Server

Câu lệnh (statement) : là một đoạn code T-SQL bất kì mà tuân theo định dạng và cú pháp để SQL Server xem là hợp lệ, chúng có thể là SELECT, UPDATE, INSERT, DELETE,..
Câu truy vấn (query) : ám chỉ câu lệnh SELECT
Mệnh đề (clause) : là những thành phần trong một câu lệnh, ví dụ như mệnh đề FROM, mệnh đề WHERE, mệnh đề JOIN, mệnh đề ORDER BY, mệnh đề SELECT
Vị từ (predicate) : có thể hiểu là một biểu thức logic dùng để lọc data hay dễ hình dung hơn là điều kiện tìm kiếm (search condition), predicate có thể có ở mệnh đề ON hoặc HAVING và thường thấy nhất là ở mệnh đề WHERE. Trong câu truy vấn trên có các predicate như “PostTypeId = 2” và “location LIKE ‘%vietnam%’ “.
Column alias (bí danh): là một tên mới chúng ta gán cho một cột nào đó vì cột đó chưa có tên do được tạo ra từ việc cộng số hoặc chuỗi, hoặc sử dụng các hàm tập hợp. “COUNT(*) AS cnt” thì cnt là tên của cột đếm sao này trong bảng kết quả trả về.
Hàm tập hợp (aggregate functions) : các hàm thực hiện tính toán tên một tập nhiều giá trị và trả về một giá trị duy nhất. Ví dụ một sốhàm như MIN(), MAX(), AVG(), COUNT() .

Trong ví dụ trên, giả sử các bạn chỉ muốn lấy ra những location có số lượng câu trả lời lớn hơn 10 thì làm sao? Các bạn có để ý là alias cnt ở mệnh đề SELECT được sử dụng ở mệnh đề ORDER BY hay không, liệu chúng ta có thể dùng nó đặt ở mệnh đề WHERE hoặc mệnh đề HAVING để loại bỏ những location nào có số lượng câu trả lời dưới 10 được không? Các bạn hãy thử viết câu truy vấn thực hiện yêu cầu trên xem sao. Câu truy vấn của mình như sau:

SELECT location, COUNT(*) AS cnt
FROM Users u
	INNER JOIN Posts p ON u.Id = p.OwnerUserId
WHERE PostTypeId = 2
GROUP BY location
HAVING location LIKE '%vietnam%' AND COUNT(*) > 10
ORDER BY cnt

Tại sao mình ko dùng cột cnt (giống như nó đang được dùng ở mệnh đề ORDER BY) để loại bỏ những dòng không thỏa mãn trên mệnh đề HAVING mà phải đếm lại số lượng post? Đơn giản rằng lúc SQL Server chạy tới mệnh đề HAVING thì chưa tồn tại column alias cnt đó đâu nha các bạn, để hiểu rõ hơn tình huống này chúng ta hãy cùng tìm hiểu trình tự thực thi các mệnh đề trong một câu truy vấn như sau.

(7) SELECT (8) DISTINCT (10) <TOP> <danh sách các cột>
(1) FROM <tên bảng 1>
(3) <kiểu join> JOIN <tên bảng 2>
(2) ON <các điều kiện join>
(4) WHERE <các điều kiện where>
(5) GROUP BY <danh sách các cột group by>
(6) HAVING <các điều kiện having>
(9) ORDER BY <danh sách các cột order by>

Để dễ tiếp cận các bạn hãy hình dung mỗi bước như vậy sẽ nhận data (dạng bảng) từ bước liền trước rồi thực hiện chức năng của bước đó trả kết quả (cũng dạng bảng) cho bước sau nó.

  1. FROM: lấy từng dòng của bảng 1 join với từng dòng của bảng 2 (cross join) và kết quả sẽ ra 1 bảng data VT1.
  2. ON: tiếp theo predicate trên mệnh đề ON sẽ được sử dụng để loại bỏ những dòng join không thỏa mãn. Như trong ví dụ của chúng ta là “u.Id = p.OwnerUserId” và những dòng thỏa điều kiện trên sẽ được insert vào bảng VT2.
  3. INNER JOIN: (ngoài ra còn có CROSS JOIN và OUTER JOIN) tùy vào kiểu join mà data ở bảng 1 hoặc bảng 2 mặc dù không thỏa mệnh đề ON vẫn có thể được insert vào bảng kết quả VT3. Nếu có nhiều hơn hai bảng ở mệnh đề FROM thì SQL Server sẽ lặp lại các bước từ 1 đến 3 cho bảng kết quả của phép join những bảng trước với bảng kế tiếp. Cứ như vậy cho đến hết các bảng trong mệnh đề FROM.
  4. WHERE: điều kiện tìm kiếm ở mệnh đề WHERE sẽ được sử dụng để lấy những dòng kết quả insert vào bảng VT4.
  5. GROUP BY: SQL Server sẽ gom nhóm các dòng data trong bảng VT4 theo danh sách các cột chỉ định trên mệnh đề GROUP BY, tạo ra bảng VT5.
  6. HAVING: điều kiện tìm kiếm ở mệnh đề HAVING được sử dụng để lấy những dòng thỏa mãn và insert vào bảng VT6.
  7. SELECT: ở bước này chỉ lấy những cột được chỉ định ở mệnh đề này và insert vào bảng VT7. Các alias được gán sẽ là tên cột của bảng VT7.
  8. DISTINCT: loại bỏ những dòng trùng lắp trong bảng VT7. kết quả sẽ là bảng VT8.
  9. ORDER BY: Những dòng trong bảng VT8 sẽ được sắp xếp theo thứ tự các cột được chỉ định trên mệnh đề ORDER BY này. Và đây là bước đầu tiên cũng là bước duy nhất có thể sử dụng column alias được tạo ra trong mệnh đề SELECT ở step 7. Bước này có xíu khác biệt so với các bước trước đó là kết quả của bước này không phải một bảng bình thường mà vì nó có thứ tự nên sẽ là một cursor, vậy nên tập kết quả này đặt tên là VC9.
  10. TOP: mệnh đề này sẽ xác định số lượng dòng sẽ được select từ VC9, kết quả sẽ là bảng VT10 và trả về cho người gọi.
SELECT location, COUNT(*) AS cnt
FROM Users u
INNER JOIN Posts p ON u.Id = p.OwnerUserId
WHERE PostTypeId = 2
GROUP BY location
HAVING location LIKE '%vietnam%' AND COUNT(*) > 10
ORDER BY cnt

Xét lại câu truy vấn trên, SQL Server sẽ lấy bảng Users join với bảng Posts (các bạn lưu ý chúng ta đang xử lý câu truy vấn ở mức luận lý nên thứ tự không thay đổi ý nghĩa, ở mức vật lý SQL Server sẽ quyết định thứ tự các bảng được truy xuất, điều này rất quan trọng vì nó ảnh hưởng trực tiếp đến tốc độ truy vấn) sau đó xét điều kiện join của mệnh đề ON để chỉ lấy những bài post của user tương ứng, sau đó loại những bài post không phải là câu trả lời bằng điều kiện PostTypeId = 2 ở mệnh đề WHERE. Tiếp đến sẽ gom nhóm các users có location giống nhau (so sánh text) rồi đếm số lượng các bài post và chỉ lấy ra những nhóm có tổng bài post nhiều hơn 10 bài. Kết quả trả về sẽ có các cột location và sốtổng lượng bài post tương ứng với location đó, sắp xếp kết quả theo thứ tự tăng dần của số bài post.

Sau khi hiểu được thứ tự thực thi các mệnh đề trong câu lệnh SELECT các bạn có thắc mắc rằng nếu điều kiện tìm kiếm trong một câu truy vấn được sử dụng càng sớm sẽ càng tốt phải không, vì khi đó chúng loại bỏ data càng sớm nên số dòng trả về cho các bước sau sẽ ít hơn thành ra chi phí tính toán xử lý sẽ ít hơn, tốc độ truy vấn sẽ nhanh hơn? Hãy nhìn lại ví dụ trên, tại sao chúng ta không đặt điều kiện tìm kiếm “location like ‘%vietnam%’ ” ở mệnh đề WHERE thay vì đặt ở mệnh đề HAVING? Liệu khi đó câu truy vấn có gì khác biệt và có chạy nhanh hơn không? Các bạn hãy chạy hai câu truy vấn dưới đây và cảm nhận sự khác biệt thử xem.

--- điều kiện tìm kiếm ở mệnh đề HAVING
SELECT location, COUNT(*) AS cnt
FROM Users u
INNER JOIN Posts p ON u.Id = p.OwnerUserId
WHERE PostTypeId = 2 
GROUP BY location
HAVING location LIKE '%vietnam%'
 AND COUNT(*) > 10
ORDER BY cnt

--- điều kiện tìm kiếm ở mệnh đề WHERE
SELECT location, COUNT(*) AS cnt
FROM Users u
INNER JOIN Posts p ON u.Id = p.OwnerUserId
WHERE PostTypeId = 2 AND location LIKE '%vietnam%'
GROUP BY location
HAVING COUNT(*) > 10
ORDER BY cnt

Để trả lời một cách đúng đắn và đầy đủ đòi hỏi chúng ta cần có thêm một vài kiến thức nền tản khác như chi phí thực thi một câu truy vấn gồm có những thông số nào, SQL Server thực thi câu truy vấn đó ra sao (hiểu execution plan), bao nhiêu tài nguyên đã dùng,…Tất cả những điều này sẽ được đề cập trong loạt bài viết hành trình đếm sao, các bạn hãy chờ xem.

Ở bài này mình đã chia sẻ với các bạn cú pháp thường sử dụng của một câu truy vấn trong SQL Server cùng với những thuật ngữ liên quan. Điều đáng chú ý hơn cả là thứ tự thực thi các mệnh đề trong câu truy vấn, hiểu được dữ liệu luân chuyển giữa các bước sẽ giúp mình viết code T-SQL chính xác và hiệu quả hơn.

Nguồn tham khảo:
1. Logical Query Processing: What It Is And What It Means to You
2. Inside Microsoft SQL Server 2008: T-SQL Querying

This Post Has 9 Comments

  1. Toan

    Chất quá!

  2. Linh

    Tks admin

Gửi phản hồi