54 điểm bởi GN⁺ 2025-10-19 | 5 bình luận | Chia sẻ qua WhatsApp
  • 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

 
aer0700 2025-10-20

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.

 
firefoxsaiko123 2025-10-20

Hừm...

 
ilikeall 2025-10-20

"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..

 
GN⁺ 2025-10-19
Ý kiến trên Hacker News
  • Tôi cứ thấy DISTINCT trong 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ả hai
    • Đôi khi DISTINCT cũ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ểu addresses_cities chỉ để ngăn việc tên thành phố bị lặp lại
    • Trải nghiệm của tôi hầu như cũng vậy. Nhưng gần đây có một trường hợp dù mọi phép JOIN đều đúng, chỉ cần thêm DISTINCT trong 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ơn
    • Tôi từng thêm LIMIT 1 và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ết
    • Tôi muốn hỏi làm sao biết được có thể bỏ DISTINCT một cách an toàn trong truy vấn SELECT x FROM t. Dù trong schema của t, x có ràng buộc PRIMARY hoặc UNIQUE, thì ai đó cũng có thể sớm bỏ ràng buộc UNIQUE đ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 rel t và thuộc tính x rồi trả về. Có thể có trùng lặp, kiểu dữ liệu cũng có thể đổi. Nếu muốn Set thì bắt buộc phải ghi rõ DISTINCT. Nếu query planner thấy ở runtime là UNIQUE hay PRIMARY thì nó sẽ không làm deduplication
    • Với Cypher thì ngược lại. Khi xử lý dữ liệu phức tạp bằng neo4j, node trùng lặp rất dễ lọt vào kết quả nên DISTINCT là bắt buộc. Đặc biệt khi dùng quan hệ có độ dài biến thiên, nếu không có DISTINCT thì vừa chậm vừa sinh nhiều bản ghi trùng
  • Tôi đã viết một tutorial dài khoảng 9000 ký tự, chia làm hai phần, về cách thiết kế cấu trúc truy vấn chính xác mà không cần DISTINCT
    https://kb.databasedesignbook.com/posts/systematic-design-of-join-queries/
    • Bài viết hay đấy. Tôi đã bookmark. Và rồi mới nhận ra đây thực ra còn là một cuốn sách
  • Một thứ không được nhắc tới nhiều là các truy vấn tìm "thứ không tồn tại". Ví dụ, dùng != hay NOT 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ý null thế nào. null có giống chuỗi rỗng không, null == null có đúng không, điều này có thể khác nhau giữa các DB
    • Về xử lý null và indexing, các DB tôi từng dùng đều không index giá trị null, nên truy vấn WHERE col IS NULL sẽ hoạt động kém hiệu quả dù col có index. Nếu thực sự cần, tôi khuyên tạo thêm một cột char(1) hoặc bit để biểu thị việc colnull hay không, rồi index trường đó
    • Bạn nói != hay NOT 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?
  • Tôi không nghĩ tất cả các 'anti-pattern' được nêu ra đều thực sự là anti-pattern. Vấn đề điều kiện truy vấn không khớp với index rốt cuộc là do chưa hiểu nguyên lý của index. Và khá nhiều vấn đề ở đây gắn với thiết kế schema cơ sở dữ liệu hơn là chính SQL. Nếu cần DISTINCT thì có thể là thiết kế primary key chưa đúng. Việc chồng quá nhiều view lê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 đề
  • Những 'anti-pattern' này thực ra chỉ là các cách lách đơn giản nảy sinh từ giới hạn trong thiết kế ngôn ngữ SQL (hoặc do thiếu thiết kế). Tôi đang làm một ngôn ngữ mới chạy trên cơ sở dữ liệu SQL, và muốn tạo ra lựa chọn tốt hơn cho từng vấn đề như thế này. Nó vẫn chưa hoàn thiện và tài liệu còn thiếu, nhưng nếu quan tâm thì tôi muốn nhận phản hồi tại https://lutra-lang.org
    • Cụm "cơ sở dữ liệu SQL" khá mơ hồ. SQL được triển khai không chỉ trong DB quan hệ mà cả một số DB phi quan hệ. Giới chuyên môn đã nhận ra vấn đề của SQL từ lâu, và cũng từng có các lựa chọn thay thế như Tutorial D của Chris Date và Hugh Darwen. Dù vậy, vì hàng chục năm mã nguồn và công cụ SQL đã tích lũy quá nhiều nên các lựa chọn thay thế không thể bén rễ. Nhờ SQL mà tôi có được sự ổn định công việc và nguồn thu đều đặn suốt hàng chục năm, nên dù đúng là cần một ngôn ngữ tốt hơn, tôi vẫn nhìn tình hình này theo hướng tích cực
    • Dự án trông khá ổn. Nếu hoàn thiện hơn nữa tôi nhất định sẽ theo dõi
  • Anti-pattern lớn nhất là không coi SQL là một ngôn ngữ lập trình thực thụ chứ không chỉ là ngôn ngữ truy vấn đơn thuần. Tôi khuyên nên thụt lề code nhất quán, nhóm các phần liên quan về mặt logic với nhau, và chuyển subquery thành CTE nếu có thể. Việc để lại comment hiệu quả cũng rất quan trọng. Đây là phong cách của tôi: https://bentilly.blogspot.com/2011/02/sql-formatting-style.html
    • Tôi nghĩ các cuộc tranh luận về code style kiểu này gần như vô nghĩa nếu không có công cụ linter phù hợp
  • Bí quyết lớn nhất giúp tôi tăng tốc truy vấn và giảm mức sử dụng tài nguyên máy chủ là làm cho truy vấn sargable hơn
    https://en.wikipedia.org/wiki/Sargable
    https://www.brentozar.com/blitzcache/non-sargable-predicates/
    • Tôi tò mò từ sargable thự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ào
    • Khi tìm nguồn gốc của từ sargable, tôi thấy câu trả lời trên StackOverflow khá hữu ích https://dba.stackexchange.com/a/217983
      Từ sargable là một portmanteau của "Search ARGument ABLE"
  • Nhiều vấn đề do lạm dụng CASE WHEN có 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 sargable
    Thay vì lạm dụng DISTINCT, một truy vấn phù hợp để de-dupe theo grain của bảng trong phần fan-out phát sinh từ join
    ROW_NUMBER() OVER (PARTITION BY <grain> ORDER BY <deterministic sort>) = 1
    
    kiểu như vậy. Một số DB còn hỗ trợ QUALIFY nên truy vấn gọn hơn rất nhiều
    Giải thích về sargable
    QUALIFY in Redshift
    • Vấn đề không sargable có thể được xử lý khá dễ bằng expression index. Ít nhất tôi nghĩ sqlite là như vậy
  • Có những tình huống mà dùng view lồ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ều view lồng nhau để tạo ra một backbone view giú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ết where khá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ục view/procedure. Với trường hợp của chúng tôi, tận dụng view lồng nhau thực tế hơn hẳn
  • Vấn đề dùng hàm trên cột có index cần được giải thích rõ hơn. Khi áp dụng hàm lên cột đã được index, hiệu quả của index biến mất và thực tế sẽ dẫn đến full scan nên chậm hơn. Tôi đã tự trải qua và rút ra bài học này
    • Có tài liệu khá nổi tiếng về chuyện này https://use-the-index-luke.com/sql/where-clause/obfuscation
    • Giải pháp được nêu ra (ví dụ: thêm index cho cột UPPER(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ằng COLLATE
      ALTER TABLE example ADD name_ci AS name COLLATE SQL_Latin1_General_CI_AS;
      
      (tùy chỉnh theo nhu cầu)
    • Blog liên quan có lỗi chính tả. Dòng đầu phải viết hoa. Nếu index đã được tạo trên dữ liệu sau khi áp dụng hàm thì khi truy vấn sẽ không phải quét toàn bộ. Dĩ nhiên trong ví dụ này, ngay từ đầu dùng collation không phân biệt hoa thường sẽ tốt hơn, nhưng nhìn chung ý tưởng cốt lõi vẫn đúng
    • "Tôi đã tự trải qua và học được điều này" nghe như khẩu hiệu của các lập trình viên SQL vậy. Dù sao thì SQL đã thay đổi rất ổn định trong thời gian dài, nên biết trước các cạm bẫy kiểu này sẽ hữu ích rất lâu
 
ahwjdekf 2025-10-21

Thiếu mất điều quan trọng nhất rồi.

  • dùng ORM