MySQL ‐ Must‐Know SQL Anti‐Patterns - woojin-playground/Backend-PlayGround GitHub Wiki

MySQL - Must-Know SQL Anti-Patterns

SQL Anti-Pattern 1: CASE WHEN and Dimension Table & View Patterns

  • 변경 비용이 선형적으로 증가한다 - 상태 코드가 하나 추가 혹은 변경되면 그 코드를 쓰는 모든 쿼리를 찾아서 수정해야 한다. 서비스가 커질수록 수정 범위를 파악하는 것 자체가 불가능하다.
  • 코드가 계약이 아니라 추측이 된다 - CASE WHEN status = 1 THEN '승인'이 코드가 팀마다 다르게 작성되는 순간 1이 무엇인지는 코드를 뜯어봐야만 알 수 있다. 즉, 명시적 계약이 없으니 해석이 팀마다 갈리게 된다.
  • 데이터 불일치는 기술적 문제가 아니라 신뢰의 문제다 - CASE WHEN 로직의 차이로 결과가 달라진다면 어느 쪽이 맞는지 판단할 기준 자체가 없어진다.

SQL Anti-Pattern 2: Problems with Using Functions on Indexed Columns and Solutions

  • 인덱스는 원본 값을 기준으로 정렬되어 있다.
  • 인덱스를 걸어둔 컬럼에 대해 가공 연산이 이루어지게 되면 옵티마이저는 인덱스에서 함수가 적용된 값을 찾을 수 없기 때문에 인덱스를 포기하고 전체 테이블을 스캔한다.
구분 인덱스 사용 함수 적용
탐색 방식 Index Seek Full Table Scan
I/O 최소화 전체 행 읽기
CPU 낮음 모든 행에 대해 함수 실행
데이터 증가 시 영향 적음 선형으로 악화
  • 애플리케이션 레이어에서 변환해서 넘기고 DB에 함수 부담을 넘기지 않으면 인덱스를 효율적으로 탈 수 있다.
  • 데이터 저장 시점에 정규화를 해서 함수가 적용되어도 인덱스를 탈 수 있게끔 하는 방식도 고려할 수 있다.

SQL Anti-Pattern 3: Problems with SELECT * in Views and Solutions

  • SELECT *로 VIEW를 만드는 것은 실무에서 가장 피해야 할 안티 패턴인데 그 이유는 다음과 같다.
    • 스키마 변경에 매우 취약하다 : SELECT *로 VIEW를 만들면 원본 테이블에 새로운 컬럼이 추가될 경우 VIEW의 구조가 의도치 않게 변경되거나 데이터베이스 시스템에 따라 에러를 발생시킬 수 있다.
    • 컬럼명 충돌 : 여러 테이블을 조인할 때 동일한 이름의 컬럼이 덮어씌워지거나 충돌할 위험이 있다.
    • 성능 낭비 : 필요 없는 데이터까지 모두 메모리에 올려야 하기 때문에 비효율적이다.
  • 위와 같은 문제들을 해결하기 위해 명시적으로 컬럼을 지정한 VIEW를 만드는 것이 좋다.
    • 안정성 확보 : 필요한 컬럼만 정확히 지정했기 때문에, 나중에 테이블에서 새로운 컬럼이 추가되더라도 이 VIEW는 전혀 영향을 받지 않고 견고하게 유지된다.
    • 명확한 구조 : 이 VIEW를 조회하는 다른 개발자나 시스템이 어떤 데이터를 받을 수 있는지 직관적으로 알 수 있다.

SQL Anti-Pattern 4: Duplicate Issues Caused by Overuse of DISTINCT and Solutions

  • SQL에서 DISTINCT는 중복된 결과값을 제거해주는 매우 편리하고 필수적인 기능이나 뚜렷한 목적없이 사용하게 되면 심각한 성능 저하와 논리적 오류 은폐라는 큰 문제를 야기할 수 있다.
    • 막대한 리소스 소모(성능 저하) : 데이터베이스가 DISTINCT를 처리하는 과정은 공짜가 아니다. 중복을 제거하기 위해 데이터베이스 엔진은 내부적으로 다음과 같은 작업을 수행하게 된다.
      • 정렬(sort) 또는 해싱(hashing) : 중복을 찾아내기 위해 조회된 모든 데이터를 메모리나 디스크에 올려두고 정렬하거나 해시 테이블을 만든다.
      • 데이터 건수가 적을 때는 티가 나지 않지만 수십만~수천만 건의 데이터를 다룰 때 DISTINCT를 사용하면 CPU와 메모리 사용량이 급증하고 쿼리 속도가 눈에 띄게 느려진다.
    • 잘못된 JOIN 조건 은폐 : 테이블 간에 JOIN을 수행할 때 조건을 잘못 설정하거나 1 : N 관계를 잘못 이해하게 되면 데이터가 의도치않게 뻥튀기가 되는데 이 때 쿼리가 왜 중복 데이터를 발생시켰는가에 대한 원인을 분석하고 JOIN 조건을 수정하는 대신 단순힌 DISTINCT`를 붙여서 눈앞의 중복만을 지워버리는 임시방편을 쓰기 쉬워지는데 이런 쿼리 자체의 근본적인 결함을 숨기기 때문에 나중에 데이터가 커지면 시스템 전체에 부하를 주는 시한폭탄으로 동작하게 된다.

SQL Anti-Pattern 5: Excessive View Layer Stacking — Problems and Solutions

  • 계층형 뷰 (Nested Views / View-on-View)
    • 장점 : 로직이 단계별로 분리되어 있어 코드를 읽기 쉽고, 특정 중간 단계의 뷰를 다른 쿼리에서 재사용하기 좋습니다.
    • 단점(성능 및 유지보수) : 최상위 뷰를 조회할 때 데이터베이스 옵티마이저(Optimizer)는 그 아래 얽혀있는 모든 하위 뷰의 쿼리를 하나하나 풀어서(View Resolution) 거대한 단일 실행 계획을 만들어야 한다. 뷰의 깊이가 깊어질수록 데이터베이스가 최적의 실행 경로를 찾지 못해 심각한 성능 저하를 유발하며 문제가 생겼을 때는 어느 단계의 뷰에서 병목이 발생했는지 추적하기도 매우 어렵다.
  • 평탄화된 뷰 (Flattened View)
    • 특징: 원본 테이블에서 직접 조인하고, 집계 함수(COUNT, SUM, AVG)와 조건문(CASE)을 한 번의 SELECT 문 안에서 모두 처리
    • 장점 : 뷰의 계층이 없으므로 옵티마이저가 쿼리의 의도를 정확히 파악하고 가장 효율적인 실행 계획을 세울 수 있다. 유지보수 시에도 여러 뷰를 옮겨 다닐 필요 없이 이 쿼리 하나만 수정하면 되므로 관리가 직관적이다.
    • 단점 : 실시간으로 원본 테이블을 조인하고 연산하므로, 데이터가 수천만 건 단위로 커지면 이 뷰를 조회할 때마다 매번 무거운 연산을 수행해야 하는 부담이 여전히 존재한다.
  • 구체화된 뷰 (Materialized View)
    • 일반적인 뷰가 '쿼리(로직)'만 저장하는 것과 달리, 구체화된 뷰는 쿼리의 '실행 결과 데이터'를 실제 디스크(스토리지)에 저장
    • 장점 : 데이터를 조회할 때마다 매번 조인과 집계를 다시 할 필요 없이, 이미 계산되어 저장된 결과 테이블을 읽기만 하면 되므로 압도적으로 빠른 조회 속도를 보장한다.
    • 단점 : 원본 데이터가 변경되어도 구체화된 뷰의 데이터는 자동으로 바뀌지 않는다. 스케줄러를 통해 주기적으로 데이터를 갱신(동기화)해 주어야 하므로, '완벽한 실시간성'이 필요한 서비스에는 적합하지 않다.