2P by GN⁺ 9시간전 | ★ favorite | 댓글 1개
  • SQL 안티패턴은 쿼리와 데이터 파이프라인의 유지보수를 어렵게 하고, 예상보다 느린 성능을 유발함
  • CASE WHEN 구문의 남용, 인덱스 컬럼에서 함수 사용, *SELECT * * 사용과 관련된 문제점 존재
  • DISTINCT로 단순히 중복 문제를 “해결”하려 하면, 근본적인 조인 오류를 숨기게 됨
  • View의 과도한 중첩깊은 서브쿼리 사용은 성능 저하와 디버깅 난이도 상승을 초래함
  • SQL을 생산 코드처럼 관리하고, 가독성을 고려한 초기 설계가 재작업을 줄여줌

소개

  • 오늘은 SQL 안티패턴 중에서 자주 발생하며 영향력이 큰 몇 가지 사례를 중심으로 설명함
  • 이러한 문제들은 데이터 신뢰 저하, 쿼리 개발 속도 저하 등 악순환을 유발함
  • 아래 목록은 모든 사례를 포괄하지 않으며, 더 깊은 이해를 원한다면 Bill Karwin의 저서 추천함

과도하게 복잡한 CASE WHEN 구문

  • 엔터프라이즈 환경에서 CASE WHEN 구문을 대규모로 사용해 상태 코드를 해석하는 경우가 많음
  • 대시보드나 리포트의 빠른 개발을 위해 하나의 View에만 해당 CASE WHEN 로직을 추가하는 것은 장기적으로 안티패턴에 해당함
  • 이는 중복 로직 복사/붙여넣기, 해석 불일치 문제를 유발하며 전체 쿼리 환경을 난잡하게 만듦
  • 방지 방법으로 차원 테이블 또는 기준 뷰를 생성해 상태 코드 해석 로직을 일관되게 공유하는 것을 권장함

인덱스 컬럼에서 함수 사용

  • SQL Server 등에서 인덱스가 걸린 컬럼에 직접 함수를 사용하면, 인덱스가 제대로 활용되지 않음
    • 예시: WHERE UPPER(name) = 'ABC'
  • 이렇게 하면 데이터베이스가 전체 테이블 스캔을 수행할 수밖에 없으므로, 성능 저하 원인이 됨
  • 인덱스를 제대로 활용하려면, 비교값을 미리 변환하거나 별도의 컬럼을 둘 필요가 있음

뷰에서 SELECT * 사용

  • 뷰 개발 시 *SELECT * *를 사용하면 편해 보이지만, 구조(스키마)가 변경되면 뷰가 쉽게 깨질 수 있음
  • 불필요한 컬럼까지 포함돼 의도치 않은 의존성 및 성능 문제가 발생함

DISTINCT 남용으로 중복 “해결”

  • SELECT DISTINCT를 단순히 사용해 중복을 제거하는 방식은, 잘못된 조인 조건이나 관계 정의 미흡에서 비롯된 근본 원인을 숨김
  • 단기적으로는 결과가 맞아 보이지만, 추후 메트릭 산출 등에서 값 불일치 문제로 이어짐
  • 올바른 방법은 조인 로직 보강을 통해 관계 정의 명확화가 선행되어야 함

뷰 계층의 과도한 중첩

  • 여러 팀이 레이어링하여 View 위에 View를 계속 쌓다 보면, 의존성 복잡도가 급증함
  • 이는 데이터베이스 성능 저하, 쿼리 간소화 및 디버깅 난이도 상승을 초래함
  • 주기적으로 변환 계층을 정리해, 무거운 로직은 베이스 View나 Table에 물리화하는 것이 바람직함

과도한 깊이의 서브쿼리

  • 서브쿼리는 초기 필터링에 유용하지만, 반복적으로 중첩됨에 따라 복잡성과 유지보수 난이도가 대폭 상승함
  • 여러 단계로 중첩된 서브쿼리는 쿼리 디버깅을 어렵게 하고, 가독성을 해침
  • 가능하면 CTE(Common Table Expression) 를 활용하여, 단계별 연산을 명확하고 읽기 쉽게 분리하는 것이 좋음

결론

  • 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하는 쿼리로는
    ROW_NUMBER() OVER (PARTITION BY <grain> ORDER BY <deterministic sort>) = 1
    
    같은 식이 유용함. 일부 DB엔 QUALIFY라는 기능이 지원되어 쿼리가 훨씬 깔끔해짐
    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은 꽤 오랜 기간 안정적으로 변화해와서, 이런 함정을 미리 아는 게 오래도록 요긴함