16.10 SQL 작성 표준

16.10.1 조인 조건은 항상 ON 절에 기재

  • MySQL의 조인 조건은 WHERE 절에 모두 모아서 표기할 수도 있고, JOIN 키워드를 이용해 각 ON절에 명시할 수도 있다.

  • SELECT *
    FROM employees e
    	LEFT JOIN dept_manager dm
    WHERE e.first_name='Smith'
    	AND dm.emp_no=e.emp_no;
    
    • 이런 형태의 쿼리는 MySQL 옵티마이저가 LEFT JOIN이 아니라 INNER JOIN으로 고쳐서 실행해 버리므로, 의도한 결과와는 다른 결과가 나오게 된다. LEFT JOIN뿐 아니라 INNER JOIN에서도 가능하면 SQL 표준 조인 표기법으로 작성하고 반드시 조인 조건은 WHERE 절이 아니라 조인의 ON 절에 명시하는 습관을 들이는 것이 좋다.

16.10.2 테이블 별칭(Alias) 사용 및 칼럼 명에 테이블 별칭 포함

  • 여러 테이블이 조인되는 쿼리에서는 테이블의 별칭을 표기하지 않으면 구분하기가 쉽지 않다. 결국은 ERD를 보거나 테이블의 스키마를 확인해야만 식별을 할 수 있다.

  • -- dept_manager 테이블에 first_name이라는 칼럼이 추가됐다고 가정하자
    SELECT first_name, last_name
    FROM employees e INNER JOIN dept_manager dm ON dm,emp_no=e,emp_no
    WHERE first_name='Smith';
    
    • dept_manager 테이블에 first_name 칼럼이 추가되는 순간부터 이 쿼리는 “칼럼명이 모호하다”라는 에러를 발생하면서 실행이 멈춰 버릴 것이다. 가능하다면 여러 테이블의 조인 여부와 상관없이 짧은 이름으로 테이블의 별칭을 부여하고, 모든 칼럼의 이름 앞에는 테이블의 별칭을 붙이는 습관을 들이자.

16.10.3 서버 사이드 프리페어 스테이트먼트 사용

  • MySQL의 JDBC 드라이버는 두 가지 방식의 프리페어 스테이트먼트 기능을 제공한다.
  • MySQL 5.0 이전 버전은 프리페어 스테이트먼트를 사용하면 MySQL 쿼리 캐시를 사용할 수 없다는 단점이 있었다. 쿼리 캐시를 위해 프리페어 스테이트먼트를 포기하기도 했지만 MySQL 5.1부터는 프리페어 스테이트먼트를 사용하는 쿼리도 쿼리 캐시를 사용할 수 있게 개선됐다.
  • MySQL의 JDBC 드라이버에서는 서버 사이드 프리페어 스테이트먼트와 클라이언트 사이트 프리페어 스테이트먼트가 있다.

16.10.4 FULL GROUP BY 사용

  • MySQL의 GROUP BY는 FULL GROUP BY의 제약이 없다. 오라클과 같은 DBMS에서는 쿼리에 GROUP BY를 사용하면 GROUP BY 절에 명시된 칼럼 이외의 모든 칼럼은 집합 함수를 통해서만 조회할 수 있는데, 이를 FULL GROUP BY라고 한다.
  • FULL GROUP BY를 사용하지 않는 쿼리는 가독성을 떨어뜨리고 사용자의 실수를 유발시킬 가능성이 높으므로 가능하다면 FULL GROUP BY 조건을 충족해서 쿼리를 작성하는 습관을 들이자.

16.10.5 DELETE, UPDATE 쿼리에서 ORDER BY .. LIMIT .. 사용 자제

  • MySQL 5.0까지는 DELETE나 UPDATE 쿼리에 ORDER BY .. LIMIT .. 형태의 쿼리를 실행해도 아무 문제가 없었다. 하지만 복제가 구축된 MySQL에서는 이러한 쿼리가 마스터와 슬레이브의 데이터를 달라지게 만들 수도 있었다.
  • 그래서 MySQL 5.1부터는 이러한 쿼리가 마스터 MySQL에서 실행되면 경고 메시지를 출력하고, 때로는 MySQL 서버가 에러를 발생시키고 쿼리를 강제 종료 할 때도 있었다.
  • 경고 메시지는 쿼리가 실행될 때마다 MySQL 서버의 에러 로그에도 기록하는데, 이렇게 쌓인 에러 로그 때문에 디스크의 여유 공간이 남지 않아서 MySQL 서버가 아무것도 처리하지 못하는 상황이 발생할 수도 있다.
  • 실제로 프라이머리 키나 유니크 키로 정렬하지 않는 이상 마스터와 슬레이브의 데이터가 달라질 가능성은 여전하기 때문에 복제를 사용하고 있는 MySQL 에서는 이런 형태의 쿼리를 사용하지 않는 것이 좋다. MySQL 5.1에서도 복제로 구축된 MySQL 서버에서 프라이머리 키로 정렬을 수행하더라도 여전히 경고 메시지를 기록하게 돼 있으므로 주의할 필요가 있다.

16.10.6 문자열 리터럴 표기는 홑따옴표만 사용

  • SQL 표준에서는 문자열 리터럴은 홑따옴표만 사용 가능하고, 쌍따옴표는 식별자에 사용하지만 MySQL에서는 문자열 리터럴 표기를 위해 쌍따옴표까지 사용할 수 있다. MySQL에서는 식별자를 표기할 때 역따옴표(`)를 사용하므로 문자열 리터럴로 표기에 홑따옴표와 쌍따옴표를 모두 사용할 수 있는 것이다. 하지만 문자열 리터럴에서 홑따옴표를 사용할 때와 쌍따옴표를 사용할 때의 문자의 이스케이프 방식이 조금 달라서 혼란을 초래할 수 있고, 이로 인해 잘못된 데이터가 저장될 가능성도 있다.
  • 문자열 리터럴은 하나만 선정해서 사용하는 것이 좋은데, 가능하다면 SQL 표준인 홑따옴표를 사용할 것을 권장한다.

16.10.7 서브쿼리는 조인으로 변경

  • 쿼리를 작성할 때 FROM 절에 사용된 괄호의 개수만큼 MySQL 서버는 임시 테이블을 만들어 처리한다고 가정할 정도로 취약하다.
  • 임시 테이블은 가급적 사용하지 않는 편이 좋은데, 이를 위해서 가장 먼저 이처럼 불필요한 FROM 절의 서브 쿼리를 제거하는 것이 좋다. 또한 MySQL의 최근 버전에서는 조인의 최적화가 상당히 높은 수준이므로 서브 쿼리보다는 조인을 사용하는 것이 여러모로 좋다. 서브쿼리로만 해결할 수 밖에 없는 요건이 아니라면 반드시 쿼리 개발 후 조인으로 다시 풀어서 작성하는 습관을 들이자.

16.10.8 UNION [ALL]은 사용 자제

  • MySQL의 UNION은 항상 내부적으로 임시 테이블을 만들어 버퍼링한 다음에 사용자에게 결과를 반환한다. 이 작업은 대량의 레코드를 처리하는 쿼리에서는 상당히 부담될 것이다. 여러 집합의 중복된 레코드를 제거해야 하는 UNION을 꼭 사용해야 한다면 특별한 우회방법은 없다. 하지만 중복 제거 없이 UNION ALL로 가능한 쿼리는 두 개의 쿼리 문장으로 분리해서 쿼리를 실행하는 편이 훨씬 더 효율적으로 처리될 수 있다.

16.10.11 UPDATE, DELETE 쿼리와 적용 건수(Affected row counts) 체크

  • 일반적으로 한 건의 레코드를 INSERT 하는 쿼리는 성공하면 1, 실패하면 0으로 처리된 레코드 건수가 에러 여부에 따라 상당히 명확하다. 하지만 UPDATE나 DELETE 문장은 쿼리의 성공적인 실행 여부를 업무적인 정상 처리 여부로 판단하기에는 부족할 수 있다. 반드시 1건이 UPDATE 되거나 DELETE 돼야 하는데, 적용된 건수는 체크하지 않고 무조건 COMMIT 해버리면 나중에 문제가 될 수도 있다.
  • 그래서 프로그램을 작성할 때는 처리된 레코드 건수를 반드시 검증하는 형태의 프로그램 로직을 추가하는 습관을 들이자.

16.10.12 숫자 값은 반드시 숫자 타입의 칼럼으로 정의

  • 처음에는 알파벳만 저장하는 용도로 CHAR 또는 VARCHAR 타입을 사용했는데, 서비스의 요건이 변경되면서 그 칼럼의 값이 숫자와 문자가 혼용되어 사용되거나 숫자만 사용되는 케이스가 자주 발생한다.
  • 문자열 타입에 숫자 값이 저장될 때, 문자열 타입에 저장된 숫자 값을 비교하기 위해 “char_type_column=2”와 같은 형태로 쿼리를 사용할 때가 많다. 하지만 이 조건을 위해 MySQL 옵티마이저는 뒤의 숫자 값을 문자열로 바꿔서 비교하는 것이 아니라 앞의 문자열 칼럼을 모두 숫자로 변환해서 비교를 수행한다. 그래서 칼럼에 인덱스가 있어도 이를 이용하지 못하고 풀 테이블 스캔을 수행하거나 인덱스 풀 스캔을 수행할 때가 많다.
  • 이런 실수를 막으려면 순수한 숫자 갑은 숫자 타입에 저장하고 알파벳이나 숫자가 혼용되는 값은 CHAR 나 VARCHAR 타입에 저장하자. 이러한 코드 형태의 값은 MySQL의 ENUM 타입을 사용하는 것도 좋은 해결책이 될 수 있다.