- Anti-pattern SQL khiến việc bảo trì truy vấn và pipeline dữ liệu trở nên khó khăn hơn, đồng thời gây ra hiệu năng chậm hơn dự kiến
- Các ví dụ tiêu biểu gồm lạm dụng CASE WHEN, áp dụng hàm lên cột có chỉ mục, SELECT *, lạm dụng DISTINCT, view và subquery lồng nhau, cấu trúc phụ thuộc quá sâu
- Phần lớn vấn đề bắt nguồn từ các giải pháp chắp vá tạm thời do áp lực tốc độ và deadline, về lâu dài làm tổn hại cả độ tin cậy dữ liệu lẫn tốc độ phát triển
- Giải pháp gồm định nghĩa join rõ ràng, tận dụng dimension table, loại bỏ lồng nhau không cần thiết, dọn dẹp view định kỳ
- Cần xem SQL không phải là script đơn giản mà là production code được quản lý ở cấp độ nhóm, và thiết kế ban đầu có tính đến tính dễ đọc sẽ giúp giảm việc làm lại
Giới thiệu
- Hôm nay, bài viết tập trung giải thích một số anti-pattern SQL thường gặp và có tác động lớn
- Những vấn đề này tạo ra vòng luẩn quẩn như suy giảm độ tin cậy dữ liệu và làm chậm tốc độ phát triển truy vấn
- Danh sách dưới đây không bao quát mọi trường hợp; nếu muốn hiểu sâu hơn, khuyến nghị đọc sách của Bill Karwin
Câu lệnh CASE WHEN quá phức tạp
- Trong các hệ thống lớn, câu lệnh CASE WHEN thường được dùng để chuyển đổi mã trạng thái (ví dụ: 1=Hết hàng) sang dạng con người có thể đọc được
- Để phát triển nhanh dashboard hoặc báo cáo, việc chỉ thêm logic CASE WHEN đó vào một View duy nhất về lâu dài là một anti-pattern
- Điều này gây ra tình trạng sao chép logic lặp đi lặp lại, diễn giải không nhất quán và khiến toàn bộ môi trường truy vấn trở nên lộn xộn
- Giải pháp là tạo riêng dimension table hoặc view dùng chung để chuyển đổi mã trạng thái và đảm bảo khả năng tái sử dụng
Dùng hàm trên cột có chỉ mục
- Nếu áp dụng hàm lên cột đã được đánh chỉ mục, như “WHERE UPPER(name) = 'ABC'”, hiệu quả của chỉ mục sẽ biến mất
- Trong SQL Server và các hệ quản trị khác, điều này có thể gây ra full table scan không cần thiết
- Giải pháp là đánh chỉ mục riêng cho cột đã qua áp dụng hàm hoặc chuyển đổi giá trị đầu vào để đơn giản hóa điều kiện truy vấn
Dùng SELECT * trong view
- Khi phát triển view, SELECT * có vẻ tiện lợi, nhưng nếu cấu trúc (schema) thay đổi thì view rất dễ bị hỏng
- Vì nó còn bao gồm cả những cột không cần thiết, nên có thể phát sinh phụ thuộc ngoài ý muốn và vấn đề hiệu năng; do đó nên chọn cột một cách tường minh
Lạm dụng DISTINCT để “giải quyết” trùng lặp
- Khi kết quả bị trùng do join sai, việc tạm xử lý bằng SELECT DISTINCT là cách che giấu vấn đề toàn vẹn dữ liệu
- Nguyên nhân gốc là điều kiện join chưa đầy đủ hoặc định nghĩa quan hệ sai (1:1, 1:N, v.v.)
- Cách giải quyết đúng là củng cố logic join để làm rõ định nghĩa quan hệ, đồng thời đảm bảo tính nhất quán quan hệ trước khi tổng hợp hoặc lập báo cáo
View lồng nhiều tầng (Excessive View Layer Stacking)
- Khi nhiều nhóm tái sử dụng view hiện có rồi tiếp tục chồng thêm view mới, chuỗi phụ thuộc sẽ trở nên phức tạp và hiệu năng suy giảm mạnh
- Việc debug trở nên khó khăn, và mở rộng truy vấn có thể khó như “khai quật khảo cổ”
- Cần có chiến lược định kỳ flatten logic chuyển đổi, đồng thời materialize các phép toán phức tạp thành base view hoặc bảng rõ ràng
Subquery quá sâu
- Subquery lồng sâu từ 3–4 tầng trở lên làm giảm tính dễ đọc và khiến việc debug trở nên khó khăn
- Thậm chí có những trường hợp phải xử lý subquery dài hơn 5000 dòng
- Tận dụng CTE(Common Table Expression) sẽ giúp phân tách các bước logic dễ hơn và tăng độ rõ ràng (readability) của truy vấn
Kết luận
- SQL nhìn bề ngoài có vẻ đơn giản, nhưng khi hệ thống mở rộng thì độ phức tạp cũng tăng theo
- Phần lớn anti-pattern không xuất phát từ ý đồ xấu mà là kết quả của sự thỏa hiệp để ra “kết quả nhanh” (tốc độ, deadline, giải pháp tạm thời)
- Nếu quản lý SQL như code (quản lý phiên bản, code review, thiết kế rõ ràng), về lâu dài có thể đồng thời đảm bảo năng suất và độ tin cậy
- Chỉ cần đầu tư vài phút cho thiết kế ban đầu, chú ý đến tính rõ ràng và nhất quán, có thể giảm đáng kể việc làm lại và sự hỗn loạn về sau
5 bình luận
Lúc này không có thời gian, nên chỉ dập lửa trước mắt rồi để sau viết lại, nhưng cứ chất đống như vậy thì sẽ thành địa ngục truy vấn kinh khủng. Tôi cũng đã tự tạo ra không ít thứ như thế. Dù biết rõ rằng cái “để sau viết lại” ấy sẽ chẳng bao giờ đến, mọi chuyện vẫn cứ thế xảy ra.
Hừm...
"Hầu hết vấn đề đều là các giải pháp tạm bợ do áp lực về tốc độ và hạn chót"
Hức hức..
Ý kiến trên Hacker News
DISTINCTtrong truy vấn là lại nghi ngờ người viết chưa thực sự hiểu mô hình dữ liệu hoặc lý thuyết tập hợp, hoặc có thể là cả haiDISTINCTcũng có thể là dấu hiệu của một schema bị chuẩn hóa quá mức. Ví dụ, tôi không nghĩ cần phải cố tạo một bảng kiểuaddresses_citieschỉ để ngăn việc tên thành phố bị lặp lạiJOINđều đúng, chỉ cần thêmDISTINCTtrong CTE là hiệu năng tăng lên đáng kể. Có vẻ như khi tính duy nhất của record được đảm bảo thì query planner sẽ tối ưu tốt hơnLIMIT 1vào truy vấn vì kỳ vọng kết quả chỉ có tối đa một dòng, và cũng nhận được phản hồi rằng cách đó không hay. Nhưng trên các bảng lớn (sqlite,mysql,postgresqlđều vậy), DB thường có xu hướng tiếp tục quét toàn bộ bảng ngay cả khi đã tìm thấy record cần thiếtDISTINCTmột cách an toàn trong truy vấnSELECT x FROM t. Dù trong schema củat,xcó ràng buộcPRIMARYhoặcUNIQUE, thì ai đó cũng có thể sớm bỏ ràng buộcUNIQUEđi. Khi đó sẽ xuất hiện bản ghi trùng và ta lại phải đau đầu tìm nguyên nhân. SQL không phải là ngôn ngữ tập hợp (set) mà là ngôn ngữ bag. Ở runtime, nó chỉ lấy reltvà thuộc tínhxrồi trả về. Có thể có trùng lặp, kiểu dữ liệu cũng có thể đổi. Nếu muốnSetthì bắt buộc phải ghi rõDISTINCT. Nếu query planner thấy ở runtime làUNIQUEhayPRIMARYthì nó sẽ không làmdeduplicationneo4j, node trùng lặp rất dễ lọt vào kết quả nênDISTINCTlà bắt buộc. Đặc biệt khi dùng quan hệ có độ dài biến thiên, nếu không cóDISTINCTthì vừa chậm vừa sinh nhiều bản ghi trùngDISTINCThttps://kb.databasedesignbook.com/posts/systematic-design-of-join-queries/
!=hayNOT IN (...)thì phần lớn trường hợp đều kém hiệu quả (dù nếu tập kết quả đã được thu hẹp đủ nhiều bởi các điều kiện khác thì vẫn có thể ổn). Và điều quan trọng là phải hiểu DB xử lýnullthế nào.nullcó giống chuỗi rỗng không,null == nullcó đúng không, điều này có thể khác nhau giữa các DBnullvà indexing, các DB tôi từng dùng đều không index giá trịnull, nên truy vấnWHERE col IS NULLsẽ hoạt động kém hiệu quả dùcolcó index. Nếu thực sự cần, tôi khuyên tạo thêm một cộtchar(1)hoặcbitđể biểu thị việccolcónullhay không, rồi index trường đó!=hayNOT IN (...)gần như luôn kém hiệu quả, tôi tò mò tại sao. Nếu giá trị bên phải là hằng số thì sẽ là tra cứu hash table và nhìn chung phải khá hiệu quả chứ, hay có lựa chọn nào còn hiệu quả hơn?DISTINCTthì có thể là thiết kế primary key chưa đúng. Việc chồng quá nhiềuviewlên nhau rốt cuộc là do các bảng nền tảng được thiết kế sai. Mô hình hóa DB tốt sẽ chặn trước mọi vấn đềsargablehơnhttps://en.wikipedia.org/wiki/Sargable
https://www.brentozar.com/blitzcache/non-sargable-predicates/
sargablethực sự được dùng trong cộng đồng nào. Tôi đã làm SQL hơn 20 năm nhưng hầu như chưa từng thấy nó trong manual, Stack Overflow hay HN. Không rõ nó được dùng nhiều trong DB nào, công ty nào, hay cộng đồng mã nguồn mở cụ thể nàosargable, tôi thấy câu trả lời trên StackOverflow khá hữu ích https://dba.stackexchange.com/a/217983Từ
sargablelà một portmanteau của "Search ARGument ABLE"CASE WHENcó thể được giải quyết bằng cách gom logic vào một UDF (User Defined Function)Việc dùng hàm trên cột có index là dấu hiệu truy vấn không
sargableThay vì lạm dụng
DISTINCT, một truy vấn phù hợp đểde-dupetheo grain của bảng trong phần fan-out phát sinh từjoinlà kiểu như vậy. Một số DB còn hỗ trợQUALIFYnên truy vấn gọn hơn rất nhiềuGiải thích về sargable
QUALIFY in Redshift
sargablecó thể được xử lý khá dễ bằngexpression index. Ít nhất tôi nghĩsqlitelà như vậyviewlồng nhau lại là cần thiết. Trong phần mềm POS của chúng tôi, chúng tôi dùng khá nhiềuviewlồng nhau để tạo ra một backboneviewgiúp nhìn giao dịch một cách gọn gàng trong một lần. Nếu không làm vậy thì phải viếtwherekhác nhau cho từng bảng, rồi mỗi lần lại xử lývoid/hoàn trả/hủy và đủ kiểu điều kiện khác. Mỗi khi có thay đổi thì phải sửa hàng chụcview/procedure. Với trường hợp của chúng tôi, tận dụngviewlồng nhau thực tế hơn hẳnUPPER(name)) ít nhất trong MS SQL Server không phải là tốt nhất. Tôi không rõ DB khác hỗ trợ ra sao, nhưng cách tốt hơn là tạo hẳn một computed column không phân biệt hoa thường bằngCOLLATE(tùy chỉnh theo nhu cầu)Thiếu mất điều quan trọng nhất rồi.