피해야 할 SQL 안티패턴들
(datamethods.substack.com)- SQL 안티패턴은 쿼리와 데이터 파이프라인의 유지보수를 어렵게 하고, 예상보다 느린 성능을 유발함
 - CASE WHEN 남용, 인덱스 컬럼에 함수 적용, SELECT *, DISTINCT 남용, 중첩된 뷰와 서브쿼리, 깊은 의존 구조 등이 대표적인 사례
 - 대부분의 문제는 속도·마감 압박으로 인한 임시방편적 해결책에서 비롯되며, 장기적으로는 데이터 신뢰성과 개발 속도를 모두 해침
 - 해결책으로 명확한 조인 정의, 차원 테이블 활용, 불필요한 중첩 제거, 주기적 뷰 정리 등이 필요
 - SQL을 단순한 스크립트가 아닌 팀 단위로 관리되는 생산 코드로 다뤄야 하고, 가독성을 고려한 초기 설계가 재작업을 줄여줌
 
소개
- 오늘은 SQL 안티패턴 중에서 자주 발생하며 영향력이 큰 몇 가지 사례를 중심으로 설명함
 - 이러한 문제들은 데이터 신뢰 저하, 쿼리 개발 속도 저하 등 악순환을 유발함
 - 아래 목록은 모든 사례를 포괄하지 않으며, 더 깊은 이해를 원한다면 Bill Karwin의 저서 추천함
 
과도하게 복잡한 CASE WHEN 구문
- 대규모 시스템에서는 상태 코드(예: 1=재고 없음)를 사람이 읽을 수 있도록 변환하기 위해 CASE WHEN 구문을 자주 사용함
 - 대시보드나 리포트의 빠른 개발을 위해 하나의 View에만 해당 CASE WHEN 로직을 추가하는 것은 장기적으로 안티패턴에 해당함
 - 이는 중복 로직 복사/붙여넣기, 해석 불일치 문제를 유발하며 전체 쿼리 환경을 난잡하게 만듦
 - 해결책은 상태 코드를 변환하는 차원 테이블(dimension table) 또는 공용 뷰를 별도로 만들어 재사용성을 확보하는 것임
 
인덱스 컬럼에서 함수 사용
- “WHERE UPPER(name) = 'ABC'”처럼 인덱스가 걸린 컬럼에 함수를 적용하면 인덱스 효율이 사라짐
- SQL Server 등에서는 이로 인해 불필요한 전체 테이블 스캔(full table scan) 이 발생함
 
 - 해결책은 함수 적용 컬럼을 별도로 인덱싱하거나 입력값을 변환해 쿼리 조건을 단순화하는 것임
 
뷰에서 SELECT * 사용
- 뷰 개발 시 SELECT * 를 사용하면 편해 보이지만, 구조(스키마)가 변경되면 뷰가 쉽게 깨질 수 있음
 - 불필요한 컬럼까지 포함돼 의도치 않은 의존성 및 성능 문제가 발생하므로, 명시적 컬럼 선택을 해야함
 
DISTINCT 남용으로 중복 “해결”
- 잘못된 조인으로 중복된 결과가 발생할 때, SELECT DISTINCT로 임시 해결하는 것은 데이터 무결성 문제를 감춤
- 근본 원인은 조인 조건의 불완전함이나 관계 정의(1:1, 1:N 등) 오류임
 - 올바른 해결은 조인 로직 보강을 통해 관계 정의를 명확화하고, 집계나 리포트 이전에 관계 정합성을 확보하는 것임
 
 
뷰 중첩(Excessive View Layer Stacking)
- 여러 팀이 기존 뷰를 재활용하며 새로운 뷰를 계속 쌓을 경우, 종속성 체인이 복잡해지고 성능이 급격히 저하됨
- 디버깅이 어려워지고, 쿼리 확장이 “고고학적 발굴” 수준으로 힘들어짐
 
 - 주기적으로 변환 로직을 평탄화(flatten) 하고, 복잡한 연산은 명확한 베이스 뷰나 테이블로 머티리얼라이즈(materialize) 하는 전략이 필요함
 
과도한 깊이의 서브쿼리
- 3~4단계 이상으로 깊게 중첩된 서브쿼리는 가독성을 떨어뜨리고 디버깅을 어렵게 함
- 5000줄 이상의 서브쿼리를 다루는 사례도 존재함
 
 - CTE(Common Table Expression) 를 활용하면 논리적 단계 구분이 쉬워지고 쿼리의 명확성(readability) 이 높아짐
 
결론
- SQL은 표면적으로 단순해 보이지만, 시스템이 커질수록 복잡성이 증대되는 특성이 존재함
 - 대부분의 안티패턴은 나쁜 의도가 아니라 ‘빠른 결과’를 내기 위한 타협의 결과(속도, 마감, 임시방편) 로 생김
 - SQL을 코드처럼 관리(버전 관리, 코드 리뷰, 명확한 설계) 하면 장기적으로 생산성과 신뢰성을 동시에 확보할 수 있음
 - 초기 설계에 몇 분 투자해, 명확성과 일관성을 고민하면 향후 재작업과 혼란이 크게 줄어듦
 
지금은 시간이 없으니까, 급한 불만 끄고 나중에 다시 쓰자고 했던 게, 쌓이다보면 끔찍한 쿼리 지옥이 되죠. 저도 여럿 만들어 봤네요. 저것들을 다시 쓸 나중은 영원히 오지 않는다는 걸 알면서도 그렇습니다.
Hacker News 의견
- 나는 쿼리에서 DISTINCT가 쓰인 걸 보면 작성자가 데이터 모델이나 집합 이론을 제대로 이해하지 못했거나, 둘 다 모를 수도 있음을 의심함
- 때론 DISTINCT는 지나치게 정규화되지 않은 스키마의 신호일 수도 있음. 예를 들어, 중복된 도시명이 기록되는 걸 막고자 굳이 addresses_cities 같은 테이블을 만들 필요가 없다고 생각함
 - 내 경험도 거의 비슷함. 하지만 최근에는 모든 조인이 올바르게 되어 있어도 CTE 안에 DISTINCT를 넣었더니 성능이 크게 좋아진 경우가 있었음. 레코드의 유일성이 보장되면 쿼리 플래너가 최적화를 하는 것 같음
 - 내가 쿼리에서 결과가 최대 한 개만 나올 거라 예상해서 LIMIT 1을 추가했더니 비슷하게 좋지 않다는 피드백을 들었음. 하지만 큰 테이블에서 (sqlite, mysql, postgresql 모두) 원하는 레코드를 찾고도 DB는 전체 테이블을 계속 탐색하는 경향이 있음
 - SELECT x FROM t 쿼리에서 DISTINCT를 안전하게 생략해도 되는지 어떻게 알 수 있을지 질문함. t의 스키마에서 x가 PRIMARY나 UNIQUE 제약을 갖는 걸 봤다 해도, 금방 누군가 UNIQUE 제약을 없앨 수도 있음. 그럼 중복이 생겨버려서 왜 그런지 고민하게 됨. SQL은 집합(set) 언어가 아니라 bag 언어임. 런타임에서 rel t와 속성 x를 찾으면 그냥 반환할 뿐임. 중복이 생길 수도, 타입이 바뀔 수도 있음. 만약 Set을 원하면 반드시 DISTINCT를 명시해야 함. 쿼리 플래너가 런타임에 UNIQUE나 PRIMARY면 deduplication을 안하게 됨
 - Cypher에서는 오히려 반대임. neo4j로 복잡한 데이터를 다룰 때, 중복 노드가 정말 쉽게 결과에 포함되는데 DISTINCT가 필수적임. 특히 가변 길이 관계를 사용할 때 DISTINCT가 없으면 느려지고 중복이 많아짐
 
 - DISTINCT 없이도 정확한 쿼리 구조를 설계하는 법에 대해 약 9000자짜리 튜토리얼을 두 파트로 써봤음
https://kb.databasedesignbook.com/posts/…- 좋은 아티클임. 북마크함. 그리고 이게 실제로 책이기도 함을 깨달음
 
 - 자주 언급되지 않은 것 중 하나가 바로 "존재하지 않는 것"을 찾는 쿼리임. 예를 들어 != 나 NOT IN (...)을 사용하는 경우가 거의 대부분 비효율적임 (다만 다른 조건에서 결과 집합이 충분히 좁아졌을 때는 괜찮을 수 있음). 그리고 DB에서 null 처리를 어떻게 하는지 이해하는 게 중요함. null과 빈 문자열이 같은 건지, null == null인지, DB마다 다를 수 있음
- null 처리와 인덱싱 관련해서, 내가 사용한 DB들은 null 값을 인덱스하지 않으므로 "WHERE col IS NULL"의 쿼리는 col 인덱스가 있어도 비효율적으로 동작함. 정말 필요하다면 col의 null 여부를 표시하는 char(1) 또는 bit 컬럼을 생성해서 그 필드를 인덱싱하는 방법을 추천함
 - != 나 NOT IN (...)이 거의 항상 비효율적이라고 했는데, 왜 그런지 궁금함. 우측값이 상수인 경우에는 해시 테이블 룩업이 되어 일반적으로 효율적일텐데, 더 효율적인 대안이 있는지 궁금함
 
 - 제시된 '안티패턴'들이 모두 진짜 안티패턴은 아니라고 봄. 쿼리 조건이 인덱스와 맞지 않는 문제는 결국 인덱스 원리 이해 부족에서 비롯됨. 그리고 여기 언급된 문제 중 상당수는 SQL 그 자체보다는 데이터베이스 스키마 설계와 더 밀접한 문제임. DISTINCT가 필요하다면 프라이머리 키 설계가 올바르지 않은 것일 수 있음. view를 너무 쌓게 되는 건 결국 기본 테이블 설계가 잘못된 것임. 좋은 DB 모델링이 모든 문제를 미리 막음
 - 이런 '안티패턴'들은 사실 SQL 언어 설계의 한계(혹은 미설계)로 인해 발생하는 단순한 우회책에 불과함. 나는 SQL 데이터베이스에서 동작하는 새로운 언어를 만들고 있어서, 이런 문제마다 좀 더 나은 대안을 만들고 싶음. 아직 미완성이며 문서화도 부족한데, 관심 있으면 https://lutra-lang.org에서 피드백 받고 싶음
- "SQL 데이터베이스"라는 표현이 모호함. SQL은 관계형뿐 아니라 비관계형 DB에도 구현된 사례가 있음. 전문가들은 이미 SQL의 문제를 오래전부터 인식하고 있었고, Chris Date와 Hugh Darwen의 Tutorial D 등 대체안도 있었음. 그럼에도 수십 년간 쌓인 SQL 코드와 도구 때문에 대체제가 자리잡지 못함. 나는 SQL 덕분에 수십년간 고용 안정과 꾸준한 수익을 얻었으니, 더 좋은 언어가 필요하긴 하지만 한편으론 이 상황을 긍정적으로 봄
 - 프로젝트가 보기 좋아보임. 더 완성도가 높아진다면 꼭 지켜보겠음
 
 - SQL을 단순한 쿼리 언어가 아니고 진짜 프로그래밍 언어로 인식하지 못하는 게 제일 큰 안티패턴임. 코드 스타일을 일관성 있게 들여쓰기하고, 논리적으로 관련 있는 부분끼리 묶길 권장함. 서브쿼리는 CTE로 바꾸길 추천함. 효과적으로 주석 남기는 것도 중요함. 내 스타일 참고: https://bentilly.blogspot.com/2011/02/sql-formatting-style.html
- 이런 코드 스타일 논쟁은 적절한 린터 툴 없이는 거의 의미 없다고 생각함
 
 - 내 쿼리 속도를 키우고 서버 리소스 사용을 줄인 가장 큰 비결은 쿼리를 좀 더 sargable하게 만드는 것임
https://en.wikipedia.org/wiki/Sargable
https://www.brentozar.com/blitzcache/non-sargable-predicates/- "sargable"이라는 단어를 실제 어떤 커뮤니티에서 쓰는지 궁금함. 20년 넘게 SQL을 다뤘지만 매뉴얼, Stack Overflow, HN 어디에서도 거의 본 적이 없음. 어떤 DB나 회사, 혹은 특정 오픈소스 커뮤니티에서 많이 쓰는지 궁금함
 - sargable의 어원을 찾다보니 StackOverflow 답변이 도움이 됨 https://dba.stackexchange.com/a/217983
"sargable"이라는 단어는 "Search ARGument ABLE"의 포터맨토(합성어)임 
 - 많은 CASE WHEN문 남용 문제는 UDF(User Defined Function)로 로직을 한 곳에 통합하면 해소할 수 있음
인덱스 컬럼에 함수를 사용하는 건 쿼리가 sargable하지 않다는 신호임
DISTINCT 남용 대신, join에서 파생된 팬아웃 중에서 테이블 그레인에 맞게 de-dupe하는 쿼리로는
같은 식이 유용함. 일부 DB엔 QUALIFY라는 기능이 지원되어 쿼리가 훨씬 깔끔해짐ROW_NUMBER() OVER (PARTITION BY <grain> ORDER BY <deterministic sort>) = 1
sargable 설명
QUALIFY in Redshift- sargable하지 않은 문제는 expression index로 쉽게 해결할 수 있음. 적어도 sqlite에선 그렇다고 생각함
 
 - view를 중첩해서 쓰는 게 오히려 필요한 상황도 있음. 우리 POS 소프트웨어에서는 트랜잭션을 한 번에 깔끔하게 볼 수 있는 백본 view를 만드는 데 중첩 view를 많이 씀. 그렇지 않으면 각각의 테이블별로 where 절을 다르게 짜고, void/반품/취소 등 각종 조건을 매번 처리해야 해서 변경 사항이 있으면 수십 개 view/procedure를 다 고쳐야 함. 우리 케이스엔 중첩 view 활용이 훨씬 실용적임
 - 인덱스 컬럼에 함수를 사용하는 문제는 명확하게 설명이 더 필요함. 인덱스가 걸린 컬럼에 함수가 들어가면, 인덱스 효율성이 사라지고 실제로는 풀스캔이 발생해서 느려짐. 이걸 직접 경험하고 체득했음
- 관련해서 잘 알려진 문서 있음 https://use-the-index-luke.com/sql/where-clause/obfuscation
 - 제시된 솔루션(예: UPPER(name) 컬럼에 인덱스 추가)은 적어도 MS SQL Server에선 최선이 아님. 다른 DB 지원 여부는 잘 모르겠지만, 더 나은 해결책은 아예 COLLATE로 케이스 인식하지 않는 계산 컬럼을 만드는 것임
(취향에 맞게 조절)ALTER TABLE example ADD name_ci AS name COLLATE SQL_Latin1_General_CI_AS; - 관련 블로그에 오타가 있음. 첫 줄은 대문자로 표기해야 함. 인덱스를 이미 함수 적용된 데이터에 걸어두면, 쿼리시 전체 스캔이 아님. 물론 이 예시에서는 애초에 케이스 인식 없는 collation을 쓰는 게 더 낫지만, 일반적으로는 본 아이디어가 타당함
 - "이걸 직접 경험하고 배웠음"은 SQL 개발자들의 모토 같음. 어쨌든 SQL은 꽤 오랜 기간 안정적으로 변화해와서, 이런 함정을 미리 아는 게 오래도록 요긴함