Quy ước định dạng SQL, và những gì chúng không bao giờ đổi

Định dạng một truy vấn lộn xộn từng bước: chữ hoa từ khoá, mỗi mệnh đề một dòng, dấu phẩy đầu hay cuối dòng, và vì sao AND/OR vẫn cần ngoặc.

Trong bài này

Phần lớn câu SQL đến tay ta dưới dạng một dòng dài: chép từ log, do ORM sinh ra, hoặc được dán vào khung chat. Định dạng lại là cách nhanh nhất để hiểu nó. Nhưng định dạng cũng có luật riêng, và các nhóm tranh cãi về chúng nhiều hơn mức cần thiết. Bài này định dạng từng bước một truy vấn trông như thật, giải thích quy ước nào quan trọng và quy ước nào chỉ là sở thích, và chỉ ra điều định dạng không bao giờ sửa được: một truy vấn mang nghĩa khác với vẻ ngoài của nó.

Các kết quả dưới đây đúng là những gì công cụ định dạng SQL tạo ra.

Điểm xuất phát

Đây là một truy vấn báo cáo như có thể thấy trong log truy vấn chậm:

select c.name, count(o.id) as orders, sum(o.total) total from customers c left join orders o on o.customer_id = c.id and o.status <> 'cancelled' where c.country = 'VN' or c.country = 'TH' and c.active = true group by c.name having count(o.id) > 2 order by total desc limit 10

Và đây là nó sau khi định dạng với thiết lập mặc định (thụt 2 dấu cách, từ khoá viết hoa, dấu phẩy cuối dòng):

SELECT
  c.name,
  count(o.id) AS orders,
  sum(o.total) total
FROM customers c
LEFT JOIN orders o
  ON o.customer_id = c.id
  AND o.status <> 'cancelled'
WHERE
  c.country = 'VN'
  OR c.country = 'TH'
  AND c.active = TRUE
GROUP BY c.name
HAVING count(o.id) > 2
ORDER BY total DESC
LIMIT 10

Những quy ước có ý nghĩa thật

Mỗi mệnh đề một dòng. SELECT, FROM, từng JOIN, WHERE, GROUP BY, HAVING, ORDER BY và LIMIT bắt đầu ở lề trái. Giờ bạn đọc được hình dạng của truy vấn mà chưa cần đọc chữ nào: chọn cột, nối một bảng, lọc, gom nhóm và sắp xếp.

Danh sách mỗi mục một dòng. Ba cột thành ba dòng. Khi ai đó thêm cột thứ tư, thay đổi trong lúc review là một dòng thêm vào thay vì một dòng bị sửa, và công cụ so sánh văn bản chỉ ra chính xác chỗ thay đổi.

Điều kiện nối nằm dưới phép nối. ON và mọi AND thuộc về phép nối được thụt vào bên dưới nó. Nhờ vậy thấy rõ o.status <> 'cancelled' là một phần của phép nối, không phải của WHERE. Vị trí này quan trọng với LEFT JOIN: nằm trong ON thì vẫn giữ những khách hàng chưa có đơn; chuyển xuống WHERE thì những khách hàng đó bị loại âm thầm, biến left join thành inner join.

Truy vấn con được thụt lề. Truy vấn lồng trong ngoặc được thụt vào một mức và các mệnh đề của nó theo cùng quy tắc:

SELECT *
FROM t
WHERE
  (a = 1 AND b = 2 OR c = 3)
  AND d IN (
    SELECT id
    FROM x
    WHERE y = 'it''s'
  )

Những quy ước chỉ là sở thích

Chữ hoa từ khoá. Viết hoa từ khoá từng là điều cần thiết khi chưa có tô màu cú pháp; ngày nay nhiều nhóm vẫn giữ thói quen này vì SQL trong mã ứng dụng thường là một chuỗi trơn không được tô màu. Công cụ cho chọn viết hoa, viết thường hoặc giữ nguyên. Nó chỉ đổi các từ khoá dành riêng: count và sum giữ nguyên, vì tên hàm và tên kiểu dữ liệu thường cũng là tên cột, còn chuỗi ký tự thì không bao giờ bị động tới ('VN' vẫn là 'VN').

Dấu phẩy đầu dòng hay cuối dòng. Với dấu phẩy đầu dòng, chữ thường và thụt 4 dấu cách, cùng danh sách cột thành:

select
    c.name
    , count(o.id) as orders
    , sum(o.total) total

Lý lẽ ủng hộ dấu phẩy đầu dòng là bạn có thể chú thích bỏ bất kỳ dòng nào trừ dòng đầu mà không làm hỏng cú pháp. Lý lẽ phản đối là trông khá lạ mắt. Hãy chọn một kiểu cho mỗi dự án và để công cụ áp dụng.

Độ rộng thụt lề. Hai dấu cách, bốn dấu cách hay tab. Cơ sở dữ liệu không quan tâm, chỉ quan trọng ở sự nhất quán.

Định dạng không bao giờ đổi nghĩa

Bộ định dạng chỉ thêm hoặc bớt khoảng trắng giữa các token và đổi chữ hoa thường của từ khoá. Nó không bao giờ được bỏ, đảo thứ tự, gộp hay tách một token. Bộ kiểm thử của công cụ kiểm tra đúng điều đó: chuỗi các token mang nghĩa trước và sau khi định dạng (bỏ chú thích và khoảng trắng, từ khoá viết thường) phải giống hệt nhau. Với truy vấn trên, cả hai đều bắt đầu bằng select, c, ., name, ,, count, (, o và giống nhau tới cuối.

Sự bảo đảm đó có hai mặt. Định dạng cũng không sửa được lỗi. Hãy nhìn lại mệnh đề WHERE:

WHERE
  c.country = 'VN'
  OR c.country = 'TH'
  AND c.active = TRUE

Đọc lên nghe như "Việt Nam hoặc Thái Lan, và đang hoạt động". Nhưng không phải. Trong SQL, AND được ưu tiên hơn OR, nên cơ sở dữ liệu hiểu là c.country = 'VN' OR (c.country = 'TH' AND c.active = TRUE): mọi khách hàng Việt Nam, dù hoạt động hay không, cộng với khách Thái Lan đang hoạt động. Bộ định dạng đặt OR và AND cùng mức thụt lề vì nó không cấu trúc lại logic của bạn. Nếu ý bạn là cách kia, hãy viết ngoặc: (c.country = 'VN' OR c.country = 'TH') AND c.active = TRUE, hoặc tốt hơn, c.country IN ('VN', 'TH') AND c.active = TRUE.

Chú thích dòng nguy hiểm trong SQL một dòng

Chú thích -- kéo dài tới hết dòng. Trong tệp đã định dạng thì vô hại, nhưng sẽ phá hoại khi ai đó ép truy vấn về một dòng:

SELECT id
FROM users
WHERE
  status = 'Active' -- only active
  AND created_at >= '2026-01-01'

Gộp thành một dòng, mọi thứ sau -- only active trở thành chú thích và điều kiện ngày biến mất mà không báo lỗi. Vì vậy bộ định dạng luôn kết thúc chú thích dòng bằng một dấu xuống dòng. Khi viết SQL có thể bị ghi log hoặc rút gọn, hãy ưu tiên chú thích /* … */.

Những lỗi thường gặp

  1. Trộn AND với OR mà không có ngoặc, rồi tin vào cách thụt lề. Định dạng thể hiện cấu trúc bạn đã viết, không phải cấu trúc bạn định viết.
  2. Chuyển điều kiện nối xuống WHERE khi dọn dẹp mã. Với LEFT JOIN, kết quả sẽ thay đổi.
  3. Nghĩ rằng tên định danh trong ngoặc sẽ được chuẩn hoá. "Order" trong PostgreSQL phân biệt hoa thường và khác với order; MySQL dùng dấu backtick. Bộ định dạng giữ nguyên mọi tên trong ngoặc, và chỉ dùng phương ngữ để biết nên chờ đợi loại dấu ngoặc và từ khoá nào.
  4. Bỏ qua cảnh báo. Chuỗi chưa đóng như 'abc được báo kèm dòng và cột (dòng 1, cột 8 trong select 'abc from t), vì mọi thứ phía sau nó đang bị đọc như văn bản.

Hãy dán truy vấn vào công cụ định dạng SQL trước khi review, và dán cả hai phiên bản vào công cụ so sánh văn bản khi muốn xem đồng nghiệp thực sự đã đổi gì.

Công cụ được nhắc trong bài

Về trang blog