IT/기초 지식

[SQL] SQL AntiPatterns

개발자 두더지 2023. 4. 12. 22:20
728x90

일본의 한 블로그 글을 번역한 포스트입니다. 오역 및 의역, 직역이 있을 수 있으며 틀린 내용은 지적해주시면 감사하겠습니다.

 

시작하기에 앞서


 빌카윈의 저서 SQL AntiPatterns 내용을 가볍게 정리해보았다. 

 

 

[1장] 무단횡단(제이워크)


1개의 칼럼에 컴마(,) 구분의 값을 넣으면 안 된다. 그 이유는 다음과 같다.

  • 검색하기 어렵다.
  • 문자열 칼럼의 문자 수 제한이라는 암묵적 제약을 받는다.
  • validation을 걸기 어렵다.

해결방법은 교차 테이블을 생성하는 것이다.

 

 

[2장] 순진한 트리


 트리형태의 계층 구조를 1개의 테이블로 표현하면 안된다. 그 이유는 다음과 같다.

  • 계층 구조가 깊어지면 그만큼 SQL을 써야한다.
  • 노드를 삭제하기 어려워진다.

해결방법은 대체 트리 모델을 사용하는 것이다.

 


[3장] 아이디가 필요해


 모든 테이블에 id를 만드는 것을 만들 필요없다. 그 이유는 다음과 같다.

  • 장황해질 가능성이 있다.
  • 아무 생각없이 id 칼럼을 만들면 논리적 일관성 문제가 생길 수 있다.
  • 시퀀스로 생성된 id == 주키가 아니다.

 해결책은 다음과 같다.

  • ORM에 의해서는 서로게이트 키의 이용을 전제하는 경우도 있기 때문에 그에 따라 만드는 것이 좋다.
  • 주키는 명확한 이름을 붙인다.
  • 규약에 얽매이지 않는다.
  • 유연한 복합 키를 사용한다.

 

 

[4장] 키가 없는 엔트리


 외부키를 제약을 사용하지 않는 패턴도 좋지 않다. 그 이유는 다음과 같다.

  • 참고 정합성을 어플리케이션에서 재구현할 필요가 있다.
  • 손상된 참고 정합성을 어떻게 할지에 대한 문제가 발생한다.
  • 애드 호크 SQL에 손상되기 쉽다. 

 해결책은 단순히 외부 키를 사용하면 된다.

 


[5장] 엔터티-속성-값


 가변 속성을 표현하기 위해 attr_name와 attr_value를 가진 테이블을 작성하는 것도 안티패턴이다. 그 이유는 다음과 같다.

(※ NULL 제약을 하지 않는다)

  • 참조 정합성을 강제할 수 없다.
  • OUTER JOIN을 사용하므로 느리다.

 해결책은 다음과 같다.

  • 스키마리스(Schema-less) 데이터는 RDB 이외의 것을 사용한다.
  • 싱글 테이블 상속, 구상 테이블 상속, 클래스 테이블 상속, 반구조화 데이터

 


[6장] 다형성 연관


 하나에 모델에 대해서 여러 개의 모델을 관련 짓기 위해 type과 id 항목을 가지는 패턴. 이유는 다음과 같다.

  • 참조 정합성이 담보되지 않는다.
  • 외부 결합이 발생한다.

해결책은 다음과 같다.

  • 교차 테이블을 만든다.
  • 공통의 부모 테이블을 만든다.

 

 

[7장] 다중 칼럼 속성


 하나의 엔티티에 여러 개의 값(예를 들어, 하나의 포스트에 대해 여러 개의 태그를 붙이는 등)을 저장하고 싶은 경우에 tag1, tag2, tag3과 같은 칼럼을 만들어 옆 방향으로 대응하는 경우도 안티패턴이다. 이유는 다음과 같다.

  • 검색하기 어렵다.
  • 갱신하기도 어렵다.
  • 일관성을 보증을 할 수 없다.
  • 사전에 정의했던 열의 숫자가 부족할 가능성이 있다.

 해결방법은 종속 테이블을 만드는 것이다.

 

 

[8장] 메타데이터 트리블


 테이블이나 열을 복사하거나, 데이터에 의존하여 테이블을 작성하는 패턴도 안 된다. 그 이유는 다음과 같다.

  • 열의 수가 많은 테이블을 생성하거나, 열의 수가 적은 여러 개의 테이블을 만들어야한다.
  • 새로운 데이터를 위해 새로운 테이블을 만들어야한다.
  • 테이블 간의 정합성을 얻기 어렵다.
  • 참고 정합성을 얻기 어렵다 (주키를 설정할 수 없다).

 해결법은 다음과 같다.

  • 행으로 분리하는 수평 파티셔닝(SQL 표준에서는 없이만 각 종 DB 각각 독자적인 방법으로 서포트하고 있다)
  • 열로 분리하는 수직 파티셔닝(특히 BLOB나 TEXT가 유효.  BLOB나 TEXT으로 SELECT * 로 데이터를 획득하는 방법은 느리다)
  • 종속 테이블의 도입

 

 

[9장] 반올림 오류


 데이터 베이스에서 소수점을 다루고 싶은 때 FLOAT형을 사용하는 패턴은 안티패턴이다. 그 이유는 다음과 같다.

  • 부동소수점의 반올림으로 인해 오차가 발생하여 계산이 맞지 않게 된다.

해결책은 다음과 같다.

  • NUMERIC형, 혹은 DECIMAL형을 사용한다.
  • FLOAT는 어림수로 다뤄야한다.

 


[10장] 31가지 맛


 열에 대입할 값을 한정하기 위해 CHECK 제약을 이용하고 싶지만, 한정할 값을 열 정의로 작성하면 좋지 않다. 그 이유는 다음과 같다.

  • 값의 폐지가 어렵다.
  • CHECK 제약, 도메인, 유저형은 각 종 데이터 베이스 제품에서 사양상 통일되어 있지 않다.

 해결책은 다음과 같다.

  • 참조 테이블을 만들어 외부 키 제약을 하는 것으로 열에 넣을 값을 제약한다. 단순한 SQL로 표현할 수 있으므로 관리가 편하다.

 

 

[11장] 유령 파일


 이미지와 같이 대용량 파일을 저장할 때 링크만을 데이터 베이스에 저장하여 파일 실체는 외부 스토리지에 저장하는 패턴도 안티패턴이다. 그 이유는 다음과 같다.

  • 레코드가 삭제된다고 해도 실제 파일까지 삭제된다고 보증할 수 없다 (외부 스토리지에 쓰레기 데이터가 남는다).
  • 롤백으로 원래대로 돌아오지 않는다.
  • 백업 툴이 서포트되지 않으므로 복원한 데이터와 이미지를 엮는 처리를 자기가 써야할 필요가 있다.
  • 외부 파일은 SQL의 액세스 권한 영향을 받지 않는다. 

 해결법은 다음과 같다.

  • 필요에 따라 BLOB 형을 검토한다.

 

 

[12장] 인덱스 샷건


 인덱스에 대한 지식 부족으로 틀린 방법으로 인덱스를 사용하는 것으로 예를 들면 다음과 같은 경우다.

  • 인덱스를 완전히 사용하지 않는다.
  • 테이블의 모든 열을 합쳐서 인덱스를 붙인다.
  • 인덱스가 듣지 않는 쿼리를 발행한다.

 안되는 이유는 다음과 같다.

  • 퍼포먼스가 안 좋아진다.

 해결책은 다음과 같다.

MENTOR 체크 리스트를 바탕으로 고찰(검증)한다.

  • Meature
  • Explain
  • Nominate
  • Test
  • Optimize
  • Rebuild

 



[13장] 모르는 것에 대한 두려움


 NULL을 포함한 열에 대해서 쿼리를 쓸 때에, NULL을 일반값으로 다루거나 혹은 일반 값을 NULL로 다루는 패턴. 이유는 다음과 같다.

  • NULL을 포함한 연산은 모두 결과가 NULL이 된다.
  • NULL은 비교할 수 없다.
  • NULL공포증에 빠져 NULL로 다뤄야할 값을 실체값으로 다룬다고 해도 사태는 나아지지 않는다.

 해결방안은 다음과 같다.

  • NULL을 임의의 값으로 다룬다.
  • 스칼라 값, 논리식으로 NULL의 연산 결과를 이해한다(직관과 다른 경우가 있다).
  • 검색은 IS NULL을 사용한다.
  • 열에 NOT NULL 제약을 부여한다. 어플리케이션 코드에 의존하지 않고, 데이터 베이스에서 일관된 제약을 강제한다.

 

 

[제14장] 애매한 그룹


 그룹 내의 최대값이 발견된 레코드의 다른 속성도 가져오려고 하는 경우. 안되는 이유는 다음과 같다.

  • GROUP BY로 열거되지 않은 열에 값이 일관되게 정해진다고 확정할 수 없다.

해결법

  • 함수 종속성이 있는 열에만 쿼리를 실행한다.
  • 상관 서브 쿼리를 사용한다.
  • 도출 테이블를 사용한다.
  • JOIN
  • 다른 열에 대해서도 집계함수를 이용한다.
  • 그룹마다 모든 값을 연계

 


[15장] 임의의 선택


 rand()를 이용하여 랜덤 정렬을 하여 맨처음의 행을 fetch하는 것 또한 안티패턴이다. 이유는 다음과 같다.

  • 인덱스가 듣지 않는다.
  • 테이블 전체를 스캔했지만 맨 처음 몇 개의 행만 사용하므로 퍼포먼스가 낮아진다.

 해결책은 다음과 같다.

  • id가 1부터 최대값까지의 사이 값을 랜덤으로 선택한다 (id가 연속된 경우에 한정)
  • 결번 뒤에 있는 키 값을 선택한다.
  • id의 리스트를 한번 전체 획득하여, 어플리케이션 쪽에서 랜덤 선택하여 한 번 더 SELECT 쿼리를 발행한다.
  • 오프셋을 사용하여 랜덤 선택지로 한다.
  • 벤더 의존 sample 함수를 사용한다.

 

 

[16장] 가난한 자의 검색 엔진


 LIKE나 정규표현을 이용한 패턴 매치는 안티패턴이다. 그 이유는 다음과 같다.

  • 인덱스의 효과가 없어지고 테이블 스캔이 된다.
  • 단순한 패턴 매치로 인해 의도하지 않은 매치가 발생한다.

 해결 방법은 다음과 같다.

  • 전문 검색 엔진을 이용한다.
  • 벤더 확장으로 전문 검색 엔진을 이용한다. MySQL이라면 풀 텍스트 인덱스를 사용하면 된다.
  • Sphinx나 Lucene(ElasticSearch)등, SQL과 독립하여 움직이는 검색 엔진을 활용한다.
  • 전치 인덱스를 자체 제작한다.

 



[17장] 스파게티 쿼리


 복잡한 문제를 하나의 SQL로 해결하려고 하는 것도 좋지 않다. 이유는 다음과 같다.

  • 메인터넌스가 어렵다.
  • 결합이나 상관 서브 쿼리등과 같은 까다로운 SQL 쿼리는 일반적으로 간단한 쿼리에 비해 느리다. 

 해결 방법은 다음과 같다.

  • 분할 통치
  • 쿼리를 여러 개로 분할하여 원스텝씩 실행한다.
  • 각각 사양에 따른 레벨로 태스크를 분할하여 처리한다.

 



[18장] 암묵적 칼럼


 SQL를 짧게 하기 위해서 SELECT *를 이용하는 것은 좋지 않다. 좋지 않은 이유는 다음과 같다.

  • 열의 추가, 삭제, 이름 변경등을 하면 쿼리 결과에 의해 발생한 변화를 코드쪽에서 제대로 다루지 않게 된다(칼럼에는 실제로는 순서가 있어, 첨자가 어긋나게 된다).
  • 모든 열 fetch하므로 데이터 양이 많아진다.

 해결법은 간단하다.

  • 열 이름을 명시적으로 지정한다.

 

 


[19장] 읽을 수 있는 패스워드


 패드워드를 평문으로 저장하는 것은 보안상 위험하다. 해결법은 다음과 같다.

  • 패스워드 해시를 저장한다.
  • 패스워드는 복구가 아닌 리셋을 한다.

 


[20장] SQL 인젝션


 동적으로 SQL을 구축할 때에는 검증하지 않은 입력을 코드로써 실행하는 것은 좋지 않다. 그 이유는 다음과 같다.

  • SQL 스테이트먼트 인수, 테이블 삭제, 사용자 행세 등 모든 것을 할 수 있기 때문이다.

 대책은 다음과 같다.

  • 프리퍼어드 스테이트먼트를 이용한다.
  • 유저의 입력을 코드로 부터 격리한다(프리페어드 스테이트먼틑를 사용할 수 없는 상황에서 유효하다).
  • 코드 리뷰

 



[21장] 가상키 편집증


 id가 연속되지 않은 것을 어떻게든 메꾸려고 하는 것은 좋지 않다. 이유는 다음과 같다.

  • 결번을 특정하기 위해 자가 결합 쿼리를 발행할 필요가 있다.
  • 배타제어의 문제로 인해 에러가 발생할 수 있다.
  • 결번을 메꾸려고 한다고 해도 시퀀스는 마지막에 생성한 값을 바탕으로 만들어지므로 새로운 결번이 만들어진다.
  • 결번은 타당한 이유로 인해 예를 들어 행이 삭제 혹은 롤백의 결과이다. 

 해결법은 다음과 같다.

  • 유사한 키를 사용하지 않는다.
  • 주키열의 값에 어떠한 의미를 갖게 해서는 안 된다.
  • GUID의 이용을 검토한다. 
  • 상사와의 대화를 통해 관리 방식을 결정하거나, 자연 키를 이용한다.

 

 

[22장] 나쁜 것 안 보기


 데이터 베이스 API의 반환값을 무시하거나, 어플리케이션 코드 안에서 존재하는 SQL만 읽거나, 예외 처리를 하지 않는 등과 같은 것은 좋지 않다. 그 이유는 다음과 같다.

  • 반환값을 무시하는 것이므로 문제를 발견할 수 없다.
  • 구축된 SQL을 보지 않고 SQL을 구축하는 코드를 쫓으므로 인해 시간을 소비한다.

대책은 다음과 값다.

  • 반환값과 예외의 체크
  • 실제로 구축된 SQL사용한다.

 



[23장] 외교적 면책특권


어플리케이션 개발의 베스트 사항을 실천하고 있는 팀이라도 SQL이라면 피하는 패턴. 결국 어떤 기술적 부담을 남기게 된다.

 해결법은 다음과 같다.

  • 데이터 베이스 개발에 있어서도 문서화, 버전 관리, 테스팅의 베스트 사항을 지킨다.
  • DDL, seed data, 데이터 베이스 관리 스크립트, 트리거와 프로시저, ER도와 문서 버전관리에 작성한다.
  • 개발, 테스트, 스테이징, 디플로이를 행하는 어플리케이션 각각에 데이터 베이스 인스턴스를 만든다. 각 개발자의 데이터 베이스를 준비한다.

 


[24장] 마법의 콩


 안티 패턴은 다음과 같다.

  • Model을 단순화하려고 한 결과, Model이 Active Record 그 자체가 되어버린 패턴
  • 컨트롤러에 비즈니스 로직이 유출되어, 코드가 덩굴처럼 얽히게 된 패턴

 좋지 않은 이유는 다음과 같다.

  • 모델이 데이터 베이스 스키마에 의존하게 된다.
  • 모델이 외부로 CRUD를 공개해버리므로 예상치 못한 방법으로 사용될 가능성이 있다.
  • 모델의 응집도를 떨어트린다.
  • 유닛 테스트가 어려워진다.

 대책은 다음과 같다.

  • 모델이 액티브 레코드를 가지도록 한다.
  • DAO를 이용하여 데이터 베이스 액세스를 분리하여, 데이터 베이스와 독립하여 유닛 테스트를 할 수 있게 된다.

참고자료

https://zenn.dev/yukito0616/articles/00ccc30b58e458

https://qiita.com/taiteam/items/33aebded2842808e0391

728x90