FullText 검색

LIKE 연산을 통해 검색을 하게 되면 인덱스를 통한 검색이 어려운데 이럴 때 고려해볼 수 있는 것이 Full-Text 검색으로FullText 검색은 단어 또는 구문에 대한 검색을 의미.

MyISAM은 MySQL 5.5 버전 이상부터, innoDB는 MySQL 5.6 버전 부터 지원.

실행 방법

MATCH … AGAINST

전체 텍스트 검색은 MATCH AGAINST 구문을 사용하여 수행.

MATCH (col1,col2,...) AGAINST (expr [search_modifier])

MATCH는 쉼표로 구분되며 검색할 열을 지정하며, AGAINST검색할 문자열과 수행할 검색 방식을 지정

검색 유형 :
{
       IN NATURAL LANGUAGE MODE
     | IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION
     | IN BOOLEAN MODE
     | WITH QUERY EXPANSION
}

FULLTEXT INDEX

MySQL에서 에서 FULLTEXT 타입의 인덱스로 Full-Text 검색을 위해서는 인덱스 설정이 필요한데

데이터 타입이 CHAR, VARCHAR, TEXT 인 경우에만 FULLTEXT INDEX 설정이 가능함.

CREATE FULLTEXT INDEX title ON news (title);

이때, 영어는 잘 검색이 되나 한글은 잘 검색이 안되는 이슈.

이를 위해 MySQL fulltext 검색 알고리즘 중, Ngram 사용.

  • MySQL은 빌트인(내장)된 Ngram parser를 지원하며, 중국어와 일본어 그리고 한글(CJK)를 지원
CREATE FULLTEXT INDEX title ON news (title) WITH PARSER ngram;

Ngram parser

일련의 텍스트를 n개의 문자로 구성된 연속된 시퀀스로 토큰화하여 검색.

ngram 파서의 기본 ngram 토큰 크기는 2(bigram)이며, 한 글자(문자)만 검색하려면 ngram_token_size를 1로 설정이 필요.

ngram_token_size = 1

이 때, ngram 파서를 사용하는 FULLTEXT 인덱스의 경우에는 아래의 구성 옵션이 무시됨.

  • innodb_ft_min_token_size / innodb_ft_max_token_size
  • ft_min_word_len / ft_max_word_len

검색 유형

1. IN NATURAL LANGUAGE 검색

검색 문자열을 단어 단위(token_size)로 분리한 후, 해당 단어 중 하나라도 포함되는 행을 찾음.
자연어 검색은 기본 검색 타입으로 MATCH … AGAINST 구문에 별도의 옵션을 지칭하지 않으면 자연어 검색 모드로 검색됨. (혹은 AGAINST 구문에 IN NATURAL LANGUAGE MODE 입력)

SELECT
  n.seq,
  n.title
FROM 
  news AS n
WHERE 
  MATCH (n.title) AGAINST ('KBO' IN NATURAL LANGUAGE MODE);

매치율

입력된 검색어의 키워드가 얼마나 더 많이 포함되어 있는지에 따라 매치율(유사성 측정값)이 결정 되는데
전체 테이블의 50% 이상의 레코드가 검색된 키워드를 가지고 있는 경우, 그 키워드는 검색어로서 의미가 없다고 판단하고 검색 결과에서 배제됨.

이 때 매치율은 row내의 고유 단어 수, 총 단어 수, 특정 단어를 포함하는 row 수 등을 기준으로 계산되며
검색 결과는 가장 높은 관련성을 가진 결과부터 자동 정렬되는데, 아래와 같은 조건에 한해 자동 정렬.

  • ORDER BY 절이 없어야 함.
  • 검색은 테이블 검색이 아닌 FULLTEXT Index를 사용하여 수행해야 함.
  • 쿼리가 테이블을 조인하는 경우, FULLTEXT Index는 조인에서 가장 왼쪽에 있는 non-constant 테이블이어야 함.

대소문자

기본적으로 검색은 대소문자를 구분하지 않는 방식으로 수행.
대소문자를 구분하는 전체 텍스트 검색을 수행하려면  binary collation을 사용하면됨.

검색어 길이

길이가 기준보다 짧거나, 특정 단어(Stopword)는 풀텍스트 검색에서 무시됨.

최소 인덱싱 글자수 설정

innodb_ft_min_token_size = 1
ft_min_word_len = 1

2. BOOLEAN 검색

불린 모드 검색은 문자열을 단어 단위로 분리한 후, 추가적인 검색 규칙을 적용되어서 단어가 포함되는 행을 찾음.
불린 모드 검색은 IN BOOLEAN MODE를 직접 지정해서 검색할 수 있으며 연산자를 사용하여 검색 조건을 추가 가능함.

SELECT
  n.seq,
  n.title
FROM 
  news AS n
WHERE 
  MATCH (n.title) AGAINST ('KBO' IN BOOLEAN MODE);

연산자

+ : AND, 반드시 포함하는 단어

– : NOT, 반드시 제외하는 단어

> : 포함하며, 검색 순위를 높일 단어

  • +mysql >tutorial
    • mysql과 tutorial가 포함하는 행을 찾을 때, tutorial이 포함되면 검색 랭킹이 높아짐

< : 포함하되,검색 순위를 낮출 단어

  • +mysql <training
    • mysql과 training가 포함하는 행을 찾지만, training이 포함되면 검색 랭킹이 낮아짐

() : 하위 표현식으로 그룹화 (포함, 제외, 순위 지정 등)

  • +mysql +(>tutorial <training)
    • mysql AND tutorial, mysql AND training 이지만, tutorial의 우선순위가 더욱 높게 지정

~ : ‘-‘ 연산자와 비슷하지만 제외 시키지는 않고 검색 조건을 낮춤

* : 와일드 카드로 붙음

“” : 구문 정의

3. with Query Expansion 검색

자연어 검색을 확장한 내용으로, 2단계에 걸쳐서 검색을 수행.

첫 단계에서는 자연어 검색을 수행한 후,
첫 번째 검색의 결과에 매칭된 행을 기반으로 검색 문자열을 재구성하여 두 번째 검색을 수행함.

쿼리 확장 검색은 IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION 혹은 WITH QUERY EXPANSION을 직접 지정해서 사용.

SELECT
  n.seq,
  n.title
FROM 
  news AS n
WHERE 
  MATCH (n.title) AGAINST ('KBO' IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION);

SELECT
  n.seq,
  n.title
FROM 
  news AS n
WHERE 
  MATCH (n.title) AGAINST ('KBO' WITH QUERY EXPANSION);

쿼리 확장 검색은 일반적으로 검색 구문이 아주 짧을 때 유용하며
만약 쿼리 확장을 사용한다면, 두 번째 검색을 할 때 에서 오타로 추측된 단어가 포함된 내용을 찾을 수 있음.

“월드컵” 이라는 검색어가 있을 경우.

자연어 모드에서는 “월”, “드”, “컵” 용어 중 일치하는 게 있으면 출력.

불린 모드에서는 “+월 +드 +컵”로 검색되어 출력.

중지 단어

mysql에서 가지고 있는 중지 단어가 36개 정도 있는데 사용자가 별도의 테이블에 중지 단어를 추가한 후에 적용할 수도 있음.

방법

  • 중지 단어를 저장할 테이블을 만드는데, 컬럼명운 무조건 value 로 지정해야하며 타입은 VARCHAR로 지정.CREATE TABLE stop_word_table (value VARCHAR(50));
  • 그리고 중지 단어를 INSERTINSERT INTO stop_word_table VALUES ('그리고'), ('매우'), ('왜냐하면');
  • 중지 단어 테이블로 사용할 테이블 지정SET GLOBAL innodb_ft_server_stopword_table = 'contents/stop_word_table'; SHOW GLOBAL VARIABLES LIKE 'innodb_ft_server_stopword_table';

중지단어도 검색을 허용하게 할 경우

innodb_ft_enable_stopword = 0

인덱스 추가 후, 다음날 쿼리를 실행시에 예상했던 인덱스를 타지 않는 현상 발생

→ ANALYZE TABLE {table_name}

Reference.

https://dev.mysql.com/doc/refman/8.0/en/fulltext-search.html
https://kabkee.github.io/mysql/mysql-full-text-search/#full-text-search-%EB%8F%84%EC%9E%85-%EA%B2%B0%EC%A0%95
https://cotak.tistory.com/158
https://heowc.dev/2021/06/17/mysql-index-statistics/

SQL AntiPatterns 개발자가 알아야할 25가지 SQL 함정과 해법

논리적 데이터베이스 설계 안티패턴

무단횡단

  • 목표 : 관련된 값의 집합을 한 칼럼에 저장한다.
  • 안티패턴 : 칼럼을 VARCHAR로 만들고 여러개의 값을 쉼표로 구분하여 넣어서 DB 변경을 최소화 한다.
  • 해법 : 다대다 관계로 교차 테이블을 생성하라.
  • Tip : 각 값은 자신의 칼럼과 행에 저장하라.

순진한 트리

  • 목표 : 재귀적 관계를 갖는 데이터를 트리나 계층적 구조로 만든다.
  • 안티패턴 : parent_id 칼럼을 추가하라.
  • 해법 : 다른 트리 모델을 사용하라.
  • Tip : 계층 구조에는 항목과 관계가 있다. 작업에 맞도록 이 둘을 모두 모델링해야 한다.

아이디가 필요해

  • 목표 : 목표는 모든 테이블이 PK를 갖도록 하는 것이지만, PK의 본질을 혼동하면 안티패턴을 초래할 수 있다.
  • 안티패턴 : 모든 테이블에 id 칼럼 추가.
  • 해법 : 상황에 따라 다르다.
    • PK 칼럼의 이름을 “id”로 짓지 말라. bugs 테이블이라면 “bug_id” 형태로 지어라.
    • 가상키(surrogate key, auto increment나 sequence등을 통해 생성되는 키)에 집착할 필요는 없다. 자연키가 적합하면 자연키를 사용하라.
    • 복합키가 적합하면 복합키를 사용하라.
  • Tip : 관례는 도움이 될 때만 좋은 것이다.

키가 없는 엔트리

  • 목표 : 데이터베이스 아키텍처를 단순화하고 싶다.
  • 안티패턴 : FK 제약조건 생략.
    • 이 경우 참조 정합성을 보정하는 코드를 직접 작성하는 책임을 져야 한다.
    • FK를 사용하지 않으면 성능이 좋아질것 처럼 말하지만, FK를 사용하지 않으면 성능 문제를 포함해 문제가 해결되기보다는 늘어난다.
  • 해법 : 참조 정합성을 강제하는 FK를 사용하라. 데이터 정합성 오류를 찾아내 정정하는 대신, 처음부터 잘못된 데이터가 입력되지 않도록 하라.
  • Tip : 제약조건을 사용해 데이터베이스에서 실수를 방지하라.

엔티티-속성-값

  • 목표 : 가변 속성 지원(보통 엔티티의 상속 구조를 표현할 때 이런 경우가 발생한다)
    • 여러 객체의 계산이나 비교를 간단히 하기 위해 객체를 하나의 데이터베이스 테이블에 행으로 저장하고 싶다. 또한 객체의 각 서브타입이 베이스 타입이나 다른 서브타입에는 적용되지 않는 속성 칼럼을 저장하는 것도 허용해야 한다.
  • 안티패턴 : 범용 속성 테이블을 사용한다.
    • 이 말은, 키-값 쌍을 저장하는 테이블을 두는 것을 뜻한다. 값은 어떤 형태가 들어올지 알 수 없으므로 충분한 크기의 문자열로 지정한다.
  • 해법 : 서브타입 모델링 기법을 사용한다.
    • 단일 테이블 상속(Single Table Inheritance)
      • 관련된 모든 타입을 하나의 테이블에 저장하고, 각 타입에 있는 모든 속성을 별도의 칼럼으로 가지도록 한다.
      • 속성 하나는 행의 서브타입을 나타내는 용도로 사용해야 한다.
      • 많은 속성이 특정 서브 타입에만 필요하고, 해당 속성이 적용되지 않는 객체를 저장하는 경우 이런 칼럼에는 NULL을 넣어준다.
      • 서브 타입의 개수가 적고, 특정 서브타입에만 속하는 속성의 개수가 적을 때, 그리고 액티브 레코드와 같은 단입 테이블 데이터베이스 접근 패턴을 사용해야 할 때 가장 좋다.
    • 구체 테이블 상속(Concrete Table Inheritance)
      • 서브 타입별로 별도의 테이블을 만든다.
      • 각 테이블에는 베이스 타입에 있는 공통 속성뿐 아니라 특정 서브타입에만 필요한 속성도 포함된다.
      • 새로운 공통 속성이 추가되면 모든 서브 타입 테이블을 변경해야 한다.
      • 모든 서브타입을 한꺼번에 조회할 필요가 거의 없을 경우에 적합.
    • 클래스 테이블 상속(Class Table Inheritance)
      • 서브타입에 공통인 속성을 포함하는 베이스 타입을 위한 테이블을 하나 만든다.
      • 각 서브타입에 대한 테이블을 만든다.
      • 서브타입 테이블의 PK는 베이스타입 테이블에 대한 FK이다.
      • 모든 서브타입에 대한 조회가 많고 공통 칼럼을 참조하는 경우가 많을 때 적합하다.
    • 반구조적 데이터(Semistructured Data)
      • 단일 테이블 상속과 유사하지만 서브타입의 속성을 칼럼으로 저장하지 않는다.
      • 데이터의 속성 이름과 값을 XML또는 JSON 형식으로 부호화해 TEXT혹은 CLOB 칼럼으로 저장.
      • 서브타입의 개수를 제한할 수 없고, 어느 때고 새로운 속성을 정의 할 수 있는 완전한 유연성이 필요할 때 적합하다.
  • Tip : 메타데이터를 위해서는 메타데이터를 사용하라.

다형성 연관

  • 목표 : 여러 부모 참조
    • 댓글 엔티티가 있을 때, 이 댓글 엔티티가 Bugs 테이블과도 일대다 관계를 맺고, FeatureRequests 테이블과도 일대다 관계를 맺고 싶은 경우.
  • 안티패턴 : 이중 목적의 FK 사용. 이를 다형성 연관(Polymorphic Associations)라고 부른다. 한 컬럼이 다른 두개 이상의 테이블의 PK값을 저장하게 만든다. 따라서 해당 컬럼에는 FK 속성을 줄 수 없다.
  • 안티 패턴이 합당한 경우 : ORM을 사용할 경우 이 안티패턴 사용이 불가피 할 수 있다. 이 경우에는 ORM이 참조 정합성을 체크하므로 그나마 괜찮다.
  • 해법 : 관계 단순화
    • 역참조
      • 교차 테이블 생성 : 각 부모에 대해 별도의 교차 테이블을 생성하고 교차 테이블에는 각 부모 테이블에 대한 FK뿐 아니라 댓글에 대한 FK도 포함시킨다.
      • 신호등 설치 : 댓글은 항상 하나의 부모에 의해서만 참조 될 수 있으므로 교차 테이블에서 comment_id에 대해 unique 제약 조건을 건다.
    • 공통 수퍼테이블 생성
      • 모든 부모테이블이 상속할 베이스 테이블을 생성한다.
      • 댓글은 베이스테이블의 ID와 일대다 관계를 맺는다.
      • 각 부모테이블은 베이스 테이블로 부터 PK를 부여 받는다.
  • Tip : 모든 테이블 관계에는 참조하는 테이블 하나, 참조되는 테이블 하나가 있다.

다중 칼럼 속성

  • 목표 : 다중 값 속성 저장. 속성이 한 테이블에 들어가야 할 것처럼 보이는데, 여러개의 값을 가진다.
  • 안티패턴 : 여러개의 칼럼 생성. 아래 테이블에서 tag가 여러개 발생하는 경우.CREATE TABLE Bugs (bug_id SERIAL PRIMARY KEY,description VARCHAR(1000),tag1 VARCHAR(20),tag2 VARCHAR(20),tag3 VARCHAR(20));
  • 해법 : 다중 값 속성을 위한 칼럼을 하나 가지는 종속 테이블 생성. 여러 개의 값을 여러 개의 칼럼 대신 여러 개의 행에 저장하는 것이다.CREATE TABLE Tags (bug_id BIGINT UNSIGNED NOT NULL,tag VARCHAR(20),PRIMARY KEY (bug_id,tag),FOREIGN KEY (bug_id) REFERENCES Bugs (bug_id));
  • Tip : 같은 의미를 가지는 각각의 값은 하나의 테이블에 저장하라.

메타데이터 트리블

  • 목표 : 확장 적응성 지원. 쿼리 성능을 향상시키고 지속적으로 크기가 늘어나는 테이블을 지원하도록 데이터베이스를 구성하는 것이다.
  • 안티패턴 : 값으로 들어가야 할 것을 테이블 이름이나 컬럼 이름으로 만드는 행위
    • 많은 행을 가진 큰 테이블을 여러개의 작은 테이블로 분리한다. 작은 테이블의 이름은 테이블 속성 중 하나의 값을 기준으로 해서 짓는다.(예: Bugs_2009, Bugs_2010, Bugs_2011,..)
    • 하나의 칼럼을 여러 개의 칼럼으로 분리한다. 칼럼 이름은 다른 속성의 값을 기준으로 해서 짓는다.(예: bugs_fiexed_2008 INT, bugs_fixed_2009 INT, …)
  • 해법 : 파티션과 정규화
    • 테이블 수평 분할 : 행을 여러 파티션으로 분리하는 규칙과 함께 논리적 테이블을 하나 정의하면 나머지는 데이터베이스가 알아서 해준다.
      • 해시 파티션 : PARTITION BY HASH(YEAR(date_reported)) PARTITIONS 4
      • 시간별로 데이터를 분할하는 경우에는 range 파티션을 사용하는 것이 일반적이다. 이 경우 시간이 지남에 따라 새로운 파티션을 추가해야 한다.
    • 테이블 수직 분할 : 칼럼으로 테이블을 나눈다. BLOG, TEXT 칼럼 같은 것을 분리한다.
    • 칼럼의 경우에는 종속 테이블을 만들어서 해당 칼럼 값을 따로 저장한다.
  • Tip : 데이터가 메타데이터를 낳도록 하지 말라.

물리적 데이터베이스 설계 안티패턴

반올림 오류

  • 목표 : 정수가 아닌 수를 저장하고 이를 산술 연산에 사용한다. 산술 연산의 결과가 정확해야 한다.
  • 안티패턴 : FLOAT,REAL,DOUBLE PRECISION 데이터 타입 사용
    • 반올림 오류가 발생한다.
  • 해법 : NUMERIC이나 DECIMAL 타입을 사용하라.ALTER TABLE Bugs ADD COLUMN hours NUMERIC(9,2);
    • NUMERIC과 DECIMAL은 유리수가 반올림되지 않고 저장된다.
  • Tip : 가능하면 FLOAT을 사용하지 말라.
  • Precision and scale : http://sqlrelay.sourceforge.net/sqlrelay/programming/precisionscale.html
    • precision 정밀도 : 소수점 앞뒤를 모두 포함한 총 자리수
    • scale : 소수점 뒤 자리수
    • 예) 112.34 : precision 5, scale 2

31가지 맛

  • 목표 : 칼럼의 값을 고정된 집합의 값으로 제한하고, 해당 칼럼이 유효하지 않은 항목을 절대로 포함하지 않는다고 보장한다.
  • 안티패턴 : 칼럼 정의에 값지정
    • CHECK 제약조건 사용
    • MySQL에서 ENUM 사용
  • 안티 패턴 사용이 합당한 경우 : 값의 집합이 변할 가능성이 거의 없을 경우.
  • 해법 : 각 값을 행으로 저장하는 색인 테이블을 만들고, 해당 행을 FK 제약조건으로 참조하도록 한다.
  • Tip : 고정된 값의 집합에 대한 유효성 확인을 할 때는 메타데이터를 사용하라. 유동적인 값의 집합에 대한 유효성을 확인할 때는 데이터를 사용하라.

유령 파일

  • 목표 : 이미지 또는 이진 데이터 저장
  • 안티패턴 : 이미지를 파일 시스템에 파일로 저장하고 파일에 대한 경로만 VARCHAR로 데이터베이스에 저장한다.
  • 해법 : 아래에 대해 모두 답할 수 있게 애플리케이션을 설계했다면 파일을 사용하고, 그렇지 못하다면 BLOB을 사용하라.
    • 데이터의 백업과 복원 절차에서 데이터베이스와 이미지 파일 관계를 제대로 복원할 수 있는가?
    • 이미지가 계속쌓이는가? 필요하지 않으면 삭제되는가? 삭제는 자동인가 수작업인가?
    • 이미지에 대한 권한 검사가 필요한가? 권한이 없는 이미지를 사용자가 요청하면 뭘보게 되는가?
    • 이미지에 대한 변경을 취소할 수 있는가? 그렇다면, 애플리케이션이 이전 상태의 이미지로 복원해야 하는가?
  • Tip : 데이터베이스 밖의 리소스는 데이터베이스가 관리하지 않는다.

인덱스 샷건

  • 목표 : 성능 최적화. 데이터베이스의 성능을 향상시키는 가장 좋은 방법은 인덱스를 잘 활용하는 것이다.
  • 안티패턴 :
    • 인덱스를 불충분하게 정의하거나 또는 아예 정의하지 않는다.
    • 너무 많은 인덱스를 정의하거나 도움이 되지 않는 인덱스를 정의한다.
    • 어떤 인덱스도 도움이 될 수 없는 쿼리를 실행한다.
  • 해법 : 인덱스를 MENTOR하라. 인덱스 샷건은 적절한 이유 없이 인덱스를 생성하거나 삭제하는 것에 대한 안티패턴이다.
    • Measure 측정 : 느린 쿼리를 찾아라.
    • Explain 실행 계획 확인
    • Nominate 지명 : 쿼리 추적 통계 정보를 모으고 쿼리를 위해 있으면 좋은 새로운 인덱스 생성 권고 등의 변경 제안
    • Test 테스트 : 인덱스를 생성한 후, 쿼리를 다시 프로파일링해야 한다. 변경으로 인해 차이가 생겼고 작업이 제대로 됐다는 것을 확인하는 것이 중요하다.
    • Optimize 최적화 : 캐시 등.
    • Rebuild 재구성 : 주기적으로 인덱스 정비. MySQL에서 ANALYZE TABLE 또는 OPTIMIZE TABLE
    • 인덱스에 대한 맹목적 추측은 좋은 전략이 아니다.
  • Tip : 데이터를 알고, 쿼리를 알고, 인덱스를 MENTOR하라.

쿼리 안티패턴

모르는 것에 대한 두려움

  • 목표 : NULL을 포함하는 칼럼에 대한 쿼리 작성
  • 안티패턴 : NULL을 일반 값처럼 사용
  • 안티 패턴 사용이 합당한 경우 : NULL을 사용하는 것은 안티패턴인 아니다. NULL을 일반적인 값처럼 사용하거나 일반적인 값을 NULL처럼 사용하는 것이 안티패턴이다.
  • 해법 : 유일한 값으로 NULL을 사용하라.
    • NULL이 연관된 연산은 대부분 결과가 NULL임을 기억할 것
    • IS NULL/IS NOT NULL/IS DISTINCT FROM(피연산자가 NULL이더라도 true/false리턴, MySQL에서는 ⇔)
    • 어떤 칼럼이 논리적인 디폴트 값을 가지지 않더라도 NOT NULL 제약조건이 필요한 경우는 정당하고 흔하다.
    • COALESCE() 함수 : 가변인수를 받아서 NULL이 아닌 첫 인수 리턴
  • Tip : 어떤 데이터 타입에 대해서든 누락된 값을 뜻하는 데는 NULL을 사용하라.

애매한 그룹

  • 목표 : 그룹의 최댓값(또는 최솟값 또는 평균 값)뿐 아니라 해당 값을 찾은 행의 다른 속성도 포함하도록 쿼리를 작성하는 것이다.
  • 안티패턴 : group 되지 않은 칼럼을 참조한다.
  • 해법 : 단일 값 규칙(SELECT 목록에 있는 모든 칼럼은 그룹당 하나의 값을 가져야 한다). 여러가지 타계책은 책에.
  • Tip : 모호한 쿼리 결과를 피하기 위해 단일 값 규칙을 따라라.

임의(random)의 선택

  • 목표 : 임의(random)의 샘플 데이터만 리턴하는 효율적인 SQL 쿼리 작성
  • 안티패턴 : 데이터를 임의로 정렬해서 가져오기.
    • RAND()와 같은 비결정적 수식으로 정렬하면 인덱스를 활용할 수 없게 된다.
  • 안티 패턴 사용이 합당한 경우 : 목록 크기도 적당하고 데이터가 늘어날 일이 거의 없는 경우라면 괜찮다.
  • 해법 : 테이블 전체 정렬을 피하라.
    • 1과 PK 최대값 사이에서 임의의 값 선택. – 단, 1과 최댓값 사이에 빈 값이 없어야 함.
    • 1과 최대값 사이에서 임의의 값을 골라 그 다음으로 큰 값 사용. – 빈틈이 드물게 존재하고 모든 키 값이 동일한 빈도로 선택되는 것이 중요하지 않을 때.
    • 모든 키 값의 모록을 구하고서 임의로 하나 고르기. – 애플리케이션 코드 이용
    • 오프셋을 이용해 임의로 고르기. 오프셋 값을 랜덤으로 생성해서 LIMIT 1 OFFSET :offset으로
    • 벤더 종속적 방법
  • Tip : 어떤 쿼리는 최적화 할 수 없다. 이 경우에는 다른 접근방법을 취해야 한다.

가난한 자의 검색 엔진

  • 목표 : 전체 텍스트 검색
  • 안티패턴 : 패턴 매칭 사용
    • LIKE ‘%crash%’
    • REGEXP ‘crash’
    • 패턴 매칭 연산자의 주요 단점 : 성능이 안 좋다.
  • 안티 패턴 사용이 합당한 경우 : 필요할 때만 가끔 사용하는 쿼리
  • 해법 :
    • SQL(또는 관계형 이론)의 기본 원리 중 하나는 칼럼에 들어 있는 값이 원자적이어야 한다는 것이다. 즉, 한 값을 다른 값과 비교할 수 있지만, 비교를 할 때는 항상 전체 값과 비교해야 한다. SQL에서 부분문자열을 비교하는 것은 비효율적이거나 부정확하다.
    • DBMS가 제공하는 풀텍스트 인덱싱 기능
    • 써드파티 검색엔진 : Sphinx Search – DB 연동 가능, Apache Lucene
    • 인덱스 테이블 직접 만들기
  • Tip : 모든 문제를 SQL로 풀어야 하는 것은 아니다.

스파게티 쿼리

  • 목표 : SQL 프로그래머들이 일하면서 가장 흔하게 수렁에 빠지는 경우 중 하나가 “이걸 어떻게 하나의 쿼리로 할 수 있을까?”하고 생각할 때다.
  • 안티패턴 : 모든 결과를 하나의 쿼리로 만들려고 한다.
  • 해법 : 여러 쿼리로 나눠라.
    • 그냥 나눠.. 뭘 고민해.
    • UNION 연산 사용
    • SQL로 SQL을 자동 생성해서 실행
  • Tip : 하나의 SQL로 복잡한 문제를 풀 수 있을 것처럼 보이더라도, 확실치 못한 방법의 유혹에 넘어가면 안된다.

암묵적 칼럼

  • 목표 : 타이핑 줄이기.
    • * 기호는 모든 칼럼을 뜻한다. 따라서 칼럼 목록은 명시적이 아니라 암시적이다.
  • 안티패턴 : 칼럼 이름 지정 없이 와일드카드(*) 사용하기
  • 안티 패턴 사용이 합당한 경우 : 임시로 사용하고 버리는 SQL
  • 해법 : 와일드카드나 암묵적 칼럼 목록에 의지하기보다는, 항상 필요한 칼럼을 나열해야 한다.
  • Tip : 원하는 대로 가져가라. 그러나 가져간 건 다 먹어야 한다.

애플리케이션 개발 안티패턴

읽을 수 있는 패스워드

  • 목표 : 패스워드를 복구하거나 재설정하기
    • 패스워드를 평문으로 저장하는 것 뿐만 아니라 평문으로 네트워크(DB 접속 포함)로 전달하는 것도 안전하지 않다.
  • 안티패턴 : 패스워드를 평문으로 저장하거나 복호화 가능하게 저장하기
  • 안티 패턴 사용이 합당한 경우 : 패스워드로 다른 써드파티에 접속해야 할 필요가 있을 경우인데, 이때에도 평문이 아니라 복호화 가능한 방식으로 암호화하여 저장해야 한다.
  • 해법 : 패스워드의 소금(SALT) 친 해시 값을 저장한다.
    • 일방향 해시 함수를 사용해 패스워드를 부호화한다. 원래 문자열로 복호화 불가능한 알고리즘을 사용한다.
    • 권장 알고리즘 : SHA-256(SHA2),SHA-384,SHA-512 …
    • MD5,SHA-1 비권장
    • 단순히 패스워드만 해시하지 말고 각 사용자별로 따로 생성한 SALT를 더해서 해싱하라.
    • DB에 SQL 전송할 때 패스워드를 넣지 않도록 하라.(미리 해싱한 값을 전송)
    • 브라우저에서 웹서버로 패스워드를 보낼 때는 HTTPS 같은 보안 프로토콜 사용. → JavaScript로 RSA 암호화 로그인
    • 패스워드 복구 금지, 이메일로 패스워드 전송 금지.
  • Tip : 당신이 패스워드를 읽을 수 있으면, 해커도 읽을 수 있다.

SQL 인젝션

  • 목표 : 동적 SQL 쿼리 작성
  • 안티패턴 : 검증되지 않은 사용자의 입력을 그대로 SQL 쿼리로 날린다.
  • 안티 패턴 사용이 합당한 경우 : 이게 합당한 경우가 어딨냐?
  • 해법 :
    • SQL 인젝션은 파싱되기 전의 SQL문을 조작하는 방법으로 동작한다. SQL문이 파싱되기 전에 동적인 부분을 삽입하는 한, (직접 쿼리를 짜던, 프레임워크를 사용하던) SQL 인젝션 위험이 있는 것이다. ORM 프레임워크 사용한다고 해서 SQL 인젝션 위험이 사라지는게 아니다.
    • 입력 값 필터링
    • 쿼리 파라미터를 통한 값 전달(PreparedStatement와 ? 쿼리 파라미터)
      • 예외 : 쿼리파라미터로 값을 설정할 경우 쿼리 옵티마이저가 제대로 최적화를 못하는 경우가 있음. 이 때는 값을 검증해서 SQL문을 조합해 낼 것.
    • 요청 파라미터로 미리 정의된 값을 검색하게 하고 이 값을 SQL 쿼리에 사용하도록 한다. – Java에서는 enum으로 허용값을 정의하면 된다.
    • 동료와 코드 검토
  • Tip : 사용자가 값을 입력하게 하라. 그러나 코드를 입력하게 해서는 안 된다.

가상키 편집증

  • 목표 : 데이터의 ID값을 빠진 숫자 없이 정돈하자
  • 안티패턴 : ID값의 틈 메우기. 틈을 메우려 드는 순간 각종 문제가 터지기 시작한다.
  • 안티 패턴 사용이 합당한 경우 : 가상키 값을 바꿔야 할 이유는 없다.
  • 해법 : 가상키의 숫자를 정렬하려는 마음을 극복하라. PK 값은 유일하고 NULL이 아니어서 각 행을 참조하는 데 사용할 수 있어야 한다. 이게 전부다. 행을 식별하는 데 연속적인 숫자일 필요는 없다.
  • Tip : 가상키를 행의 식별자로 사용한다. 가상키는 행 번호가 아니다.

나쁜 것 안 보기

  • 목표 : 코드를 적게 작성하기
  • 안티패턴 : 데이터베이스 API 리턴 값 무시하는 코드. 애플리케이션 코드에 산재해 있는 SQL 코드의 단편만 보기
  • 안티 패턴 사용이 합당한 경우 : 객체지향언어에서 문제 발생시 예외를 던지므로 모든 문장에 대해 에러 여부를 고민하지 않아도 된다.(PHP 등의 일부 언어는 에러가 발생하면 예외를 던지지 않고 리턴 값에 예외 코드를 넣기 때문에 각각의 문장 실행후 예외 코드를 검사해야 하는 것으로 보임).
  • 해법 :
    • 에러 검사 코드 작성
    • 애플리케이션 코드에 중간중간 있는 SQL말고 실제 실행된 SQL 코드 확인
  • Tip : 코드의 문제를 해결하는 것만으로도 이미 충분히 어렵다. 보지 않고 작업해 스스로를 방해하지 말라.

외교적 면책특권

  • 목표 : 프로그램 개발의 관례 따르기. 대부분의 노련한 개발자들은 편의를 위해 이런 관례를 희생하는 것이 실패의 지름길임을 안다.
  • 안티패턴 : SQL은 프로그램이 아니라고 생각하고 일반적인 관례를 따르지 않는 경우.
  • 안티 패턴 사용이 합당한 경우 : 한 번 사용하고 바로 삭제해도 되는 임시 SQL를 만들 경우
  • 해법 : 초당적 품질 문화 확립. SQL도 일반적인 프로그래밍의 관례인 문서화, 버전관리, 자동화된 테스틀 하라.
  • Tip : 애플리케이션뿐만 아니라 데이터베이스에 대해서도 문서화, 테스트, 소스 코드 관리와 같은 소프트웨어 개발 관례를 사용하라.

마법의 콩

  • 목표 : MVC에서 모델 단순화하기
  • 안티패턴 : MVC 애플리케션에서 모든 모델 클래스가 액티브 레코드(ActiveRecord) 클래스를 상속하는 관행
  • 안티 패턴 사용이 합당한 경우 : 프로토타이핑
  • 해법 : 액티브 레코드를 가지는 모델
    • 모델과 액티브 레코드와 같은 DAO 사이의 관계는 IS-A(상속)가 아닌 HAS-A(집합연관)여야 한다. 액티브레코드를 사용하는 대부분의 프레임워크는 IS-A 관계를 가정한다.
  • Tip : 테이블에서 모델을 분리하라.

이 항목은 따로 설명하면, 액티브 레코드가 나쁘다는 얘기가 아니다.컨트롤러에서 DAO 혹은 액티브 레코드의 DB관련 메소드(혹은 그 외의 데이터 접근에 직접 관련된 메소드)를 직접 호출하지 말라는 얘기이다. Java로 설명하면 Controller는 Service를 호출하고 Service에서 DAO를 호출해야 한다. Controller에서 DAO를 직접 호출되면 안된다. 이 원칙을 액티브 레코드를 사용하는 MVC 프레임워크에서도 그대로 적용하며 Controller에서 ActiveRecord의 데이버테이스 접근 메소드를 직접 호출하지 말고 중간 모델 객체(서비스 객체)를 두라는 의미이다.

책 외의 다른 정보

EXIST()를 사용할 곳에 COUNT() 사용금지

IS NULL, IS NOT NULL

NULL
– DB에서 NULL이 일반적인 값과는 다르게 취급된다.
– 아직 정의되지 않은 값.
– 0(숫자) 또는 공백(문자)과 다르다.
– 테이블을 생성할 때 NOT NULL 또는 PRIMARY KEY로 정의되지 않은 모든 데이터 유형은 널 값을 포함할 수 있다.
– NULL 값을 포함하는 연산의 경우 결과 값도 NULL이다. 모르는 데이터에 숫자를 더하거나 빼도 결과는 마찬가지로 모르는 데이터인 것과 같다.
– 결과값을 NULL이 아닌 다른 값을 얻고자 할 때 NVL/ISNULL 함수를 사용한다.
– NULL 값의 대상이 숫자 유형 데이터인 경우는 주로 0(Zero)으로, 문자 유형 데이터인 경우에는 블랭크보다는 ‘x’ 같은 해당 시스템에서 의미없는 문자로 바꾸는 경우가 많다.

IS NULL

-- NULL인 데이터
SELECT *
FROM test
WHERE score IS NULL;

IS NOT NULL

-- NULL이 아닌 데이터
SELECT *
FROM test
WHERE score IS NOT NULL;

IFNULL

-- NULL이면 0으로 치환.
SELECT IFNULL(score, 0) FROM test;

COALESCE

-- NULL이면 0으로 치환.
SELECT COALESCE(score, 0) FROM test;
IFNULL과 COALESCE 차이.
COALESCE() is in all the major databases, while IFNULL() isn't in SQL Server which uses ISNULL()instead. Performance should be no different. –  Hart CO Nov 12 '14 at 18:56
결과는 같지만. COALESCE가 모든 데이터베이스에서 사용가능하니 COALESCE를 권장한다.