DB 를 MySQL 5.7 에서 8.0 으로 올리게 됐다. 대상은 같은 DB 를 쓰는 서비스 4개.

업그레이드 준비라고 하면 보통 “8.0 에서 에러 나는 쿼리 찾기”를 떠올린다. 해 보니 그건 쉬운 쪽이었다. 에러는 테스트하면 바로 보인다. 무서운 건 에러 없이 결과만 달라지는 쿼리였다.


먼저 범위를 잡았다

대상 개수
MyBatis 매퍼 파일 109
JPA 어노테이션 (엔티티 · 네이티브 쿼리) 374
DDL 파일 16
DB 서버 변수 실측 11종

쿼리는 정적 스캔으로 훑고, 서버 변수는 실제 DEV 서버에서 값을 뽑았다. 둘 다 해야 했다. 코드만 보면 서버가 어떤 sql_mode 로 돌고 있는지를 모른다.


1. 바로 터지는 것

GROUP BY … DESC

5.7 까지는 GROUP BY 에 ASC / DESC 를 붙일 수 있었다. 8.0 에서는 문법 에러다.

-- 5.7: 됨 / 8.0: 문법 에러
SELECT ym, COUNT(*) FROM monthly_stat GROUP BY ym DESC;

-- 고친 것
SELECT ym, COUNT(*) FROM monthly_stat GROUP BY ym ORDER BY ym DESC;

서버 설정 세 가지

  • sql_mode 에서 ONLY_FULL_GROUP_BY 를 뺀 커스텀 설정 — 8.0 기본값으로 올라가면 SELECT 에 집계 안 된 컬럼이 있는 쿼리가 ERROR 1055 로 깨진다.
  • NO_AUTO_CREATE_USER — 8.0 에서 없어진 모드다. 설정 파일에 남아 있으면 서버가 아예 안 뜬다.
  • lower_case_table_names — 8.0 은 데이터 디렉터리를 처음 만들 때만 정할 수 있다. 나중에 바꿀 수 없다.

설정은 DB 담당자에게 이관안 5종으로 정리해 넘겼다. 코드에서 할 일은 ONLY_FULL_GROUP_BY 에 걸리는 쿼리를 미리 고치는 것이었다.


2. 조용히 틀어지는 것 — 진짜 문제

암묵적 정렬이 사라진다

5.7 은 GROUP BY 결과를 그룹 키 순서로 정렬해서 돌려줬다. 8.0 은 안 한다.

SELECT DATE_FORMAT(created_at, '%Y-%m') AS ym, COUNT(*)
FROM study_log
GROUP BY ym;
-- 5.7: 2026-01, 2026-02, 2026-03 … 순서로 나옴
-- 8.0: 순서 보장 없음

에러는 안 난다. 테스트도 통과한다. 그런데 월별 차트의 막대 순서가 뒤섞인다. ORDER BY 가 빠진 쿼리 2곳을 찾아 넣었다.

정렬 없는 페이징

SELECT * FROM content WHERE subject = ? LIMIT 20 OFFSET 40;

ORDER BY 없는 LIMIT 은 어느 버전에서든 순서를 보장하지 않는다. 지금까지는 실행 계획이 우연히 같아서 같은 순서가 나왔을 뿐이다. 엔진이 바뀌면 같은 행이 두 페이지에 나오거나, 어떤 행은 영영 안 나올 수 있다. 3곳에 정렬 기준(PK 포함)을 넣었다.

덤: 인덱스를 못 타던 날짜 조건

-- 문자열 컬럼을 함수로 감싸서 인덱스를 못 탐
WHERE DATE(plan_date) = '2026-09-22'

-- 범위 조건으로
WHERE plan_date >= '2026-09-22' AND plan_date < '2026-09-23'

EXPLAIN 기준 index → range, 스캔 행이 21.3만 → 10.6만 으로 절반이 됐다.


검증: 8.0 을 기다리지 않고 8.0 처럼 돌려 보기

5.7 세션에서 모드만 8.0 기본값처럼 켜고 돌렸다.

SET SESSION sql_mode = 'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';
  • 고치기 전: ERROR 1055
  • 고친 뒤: 통과

정적 스캔을 믿으면 안 되는 순간

스캔 도구가 “최고 위험” 으로 표시한 곳이 4개 있었다. 쿼리 안에서 사용자 변수(@var := …)에 값을 넣고 같은 쿼리에서 다시 읽는 패턴인데, 8.0 에서 동작이 달라질 수 있다는 경고였다.

바로 고치지 않고 세 가지를 확인했다.

  1. 데이터 — 그 변수가 실제로 결과를 바꾸는 데이터가 있는가
  2. 호출 로그 — 그 쿼리가 지금도 불리는가
  3. 소비 측 — 결과를 받는 쪽이 그 값을 쓰는가

네 곳 모두 수정할 필요가 없었다. 반대로, “하드코딩 값”이라고 잡힌 곳을 스캔 결과대로 고쳤다면 어떤 연령의 콘텐츠가 144건에서 2건으로 줄어 버릴 뻔한 오탐도 있었다. 실제 데이터로 돌려 보고 나서야 알았다.


덤 2: 몇 달째 조용히 실패하던 UPDATE

점검하다가 월간 리포트를 갱신하는 UPDATE 하나가 7개월째 실패하고 있던 걸 발견했다. 없는 컬럼을 참조하고 있었다. 실패해도 화면에 바로 티가 나지 않는 작업이라 오래 묻혀 있었다.

같은 유형이 더 있는지 보려고 네이티브 SQL 에서 참조하는 (테이블, 컬럼) 175쌍을 information_schema.COLUMNS 와 대조했다. 0건이었다.

SELECT TABLE_NAME, COLUMN_NAME
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = ? AND (TABLE_NAME, COLUMN_NAME) IN ( … );

배운 것

  • 업그레이드 준비의 핵심은 “에러 나는 쿼리”가 아니라 “결과가 바뀌는 쿼리”다. 에러는 테스트가 잡아 주지만, 순서가 바뀐 차트는 사용자가 잡는다.
  • 5.7 의 GROUP BY 정렬, ORDER BY 없는 LIMIT — 지금까지 “우연히” 맞던 것들이 8.0 에서 깨진다.
  • 서버 설정은 코드가 아니라 서버에서 실측하자. NO_AUTO_CREATE_USER 하나로 DB 가 안 뜰 수 있다.
  • 정적 스캔 결과는 후보 목록이지 할 일 목록이 아니다. 데이터 · 호출 · 소비 측으로 확인하고 고쳐야 오탐 수정이 장애가 되지 않는다.