Flyway로 MySQL 저장 프로시저를 넣었더니 syntax error near '' at line 80으로 배포가 멈췄다. 80번 줄은 멀쩡했고, 그 줄만 떼어 DB에서 직접 실행하면 잘 만들어졌다.
원인은 SQL이 아니라 Flyway 8.0의 MySQL 파서였다. END REPEAT를 프로시저의 끝으로 잘못 읽고 그 앞에서 문장을 잘라 버린다.
에러는 DB가 내지만 원인은 파서에 있다. 그래서 SQL을 아무리 봐도 안 보인다.
어떤 구조에서 나나
스키마 변경을 Flyway로 관리한다. 앱이 뜰 때 미적용 마이그레이션을 순서대로 자동 실행하고, 이력은 FLYWAY_SCHEMA_HISTORY에 남는다. 별도 CLI를 돌리지 않는다.
테이블만 다룰 때는 문제가 없었다. 저장 프로시저와 이벤트를 넣으면서 겪은 것들이다.
집계용 프로시저를 R__ 파일로 넣고 배포했더니 이렇게 죽었다.
syntax error near '' at line 80
프로시저 안에 이런 구조가 있었다.
CREATE PROCEDURE SP_EXAMPLE()
BEGIN
...
REPEAT
...
IF ... THEN
...
END IF; -- ← 여기 세미콜론
UNTIL done END REPEAT;
END;
파서가 END REPEAT를 프로시저의 끝으로 잘못 읽는다. 그래서 그 앞의 ;에서 문장을 끊어 버리고, 잘린 조각을 DB에 보낸다. DB는 당연히 문법 오류를 낸다.
해결 — 커서와 루프를 안 쓴다
프로시저를 평평하게 다시 썼다. REPEAT·WHILE·커서를 전부 없애고 DELETE + INSERT ... SELECT 두 문장으로 바꿨다.
CREATE PROCEDURE SP_EXAMPLE()
BEGIN
DECLARE v_base VARCHAR(8);
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET v_base = DATE_FORMAT(ADDDATE(NOW(), INTERVAL -1 DAY), '%Y%m%d');
DELETE FROM SUMMARY_TABLE;
INSERT INTO SUMMARY_TABLE (...)
SELECT ... FROM ( ... UNION ALL ... ) V
GROUP BY ...;
END;
점포별로 끊어 커밋하려고 루프를 넣었던 것인데, 1편의 READ COMMITTED로 원본 락이 사라지니 나눌 이유 자체가 없어졌다.
지금은 파일 맨 위에 이유를 박아 뒀다.
-- 커서·REPEAT·WHILE 은 쓰지 않는다. Flyway 8.0 의 MySQL 파서가 END REPEAT 를
-- 프로시저 끝으로 잘못 읽어 생성이 깨진다.
주석이 없으면 다음 사람이 똑같이 루프를 넣는다. 파서 문제라 코드만 봐서는 알 수 없다.
실패한 이력 행을 지워야 다시 시도된다
실패하면 FLYWAY_SCHEMA_HISTORY에 SUCCESS = 0 행이 남는다. 파일을 고쳐도 그 행이 있으면 validate가 막는다. 그 행을 지우고 다시 배포해야 한다.
CREATE INDEX IF NOT EXISTS가 없다
이건 구조 검사로 안 잡히고 실제 DB에 두 번 돌려야 발견된다.
우리 규칙은 신규 테이블 묶음을 한 파일에 몰아 넣는 것이다. MySQL은 DDL 롤백이 안 되니 파일 중간에서 실패하면 부분 적용이 남는다. 그래서 모든 DDL을 멱등하게 쓴다.
CREATE TABLE IF NOT EXISTS FOO ( ... );
여기까지는 된다. 문제는 인덱스다. MySQL에는 CREATE INDEX IF NOT EXISTS가 없다. 분리해 두면 재실행 시 이렇게 죽는다.
ERROR 1061 (42000): Duplicate key name 'IDX_FOO_BAR'
멱등성이 깨진다. 한 파일에 몰아 넣은 전제가 무너진다.
그래서 신규 테이블의 인덱스는 CREATE TABLE 안에 인라인으로 넣는다.
CREATE TABLE IF NOT EXISTS USER_GRADE_TOTAL
(
STORE_CODE VARCHAR(5) NOT NULL,
USER_ID VARCHAR(45) NOT NULL,
TOTAL_PAY BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (STORE_CODE, USER_ID),
KEY IDX_USER_GRADE_TOTAL_PAY (STORE_CODE, TOTAL_PAY)
);
기존 테이블에 인덱스를 추가하는 건 별개다. 그건 작은 마이그레이션으로 따로 뺀다.
그 밖에 걸렸던 것
함수·트리거에는 특성 선언이 필요하다
RETURNS 뒤, BEGIN 앞에 NO SQL이나 READS SQL DATA를 안 붙이면 이렇게 거부된다.
ERROR 1418: This function has none of DETERMINISTIC, NO SQL, or READS SQL DATA
in its declaration and binary logging is enabled
바이너리 로깅이 켜진 서버에서만 나온다. 개발에서 통과하고 운영에서 터질 수 있다.
CREATE OR REPLACE가 없다
MySQL은 프로시저·함수에 CREATE OR REPLACE를 지원하지 않는다. R__ 파일은 항상 이 형태다.
DROP PROCEDURE IF EXISTS SP_EXAMPLE;
CREATE PROCEDURE SP_EXAMPLE() ...
버전 번호를 정수로 쓰지 않는다
V3, V4처럼 순번을 쓰면 브랜치가 갈릴 때 반드시 충돌한다. 두 사람이 각자 V5를 만든다. 타임스탬프로 바꿨다.
date +%Y%m%d%H%M%S # → V20260826160001__create_user_grade_total.sql
성공한 V 파일은 절대 고치지 않는다
체크섬이 바뀌어 validate가 실패한다. 고칠 게 있으면 새 파일로 앞으로 고친다(forward-fix).
반대로 R__는 고치는 게 정상이다. 체크섬이 바뀌면 자동으로 다시 적용된다. 함수·프로시저·뷰는 새 파일을 만들지 말고 기존 R__를 수정한다.
파괴적 변경은 두 번에 나눈다
EC2를 순차 배포하므로 옛 코드가 새 스키마를 보는 구간이 반드시 생긴다. 컬럼을 지우거나 이름을 바꾸면 그 구간에서 옛 코드가 죽는다.
그래서 expand/contract로 나눈다.
- 추가 — 새 컬럼을 만든다. 옛 코드는 모르니 영향 없다
- 전 서버에 새 코드 배포
- 제거 — 다음 배포에서 옛 컬럼을 지운다
한 번에 하면 배포 중간에 장애가 난다.
정리
- Flyway 8.0의 MySQL 파서는
REPEAT/WHILE/커서를 만나면 프로시저를 중간에서 자른다. 에러는 DB가 내지만 SQL은 멀쩡하다. - 실패하면
SUCCESS = 0이력 행을 지워야 재시도된다. CREATE INDEX IF NOT EXISTS가 없다. 신규 테이블 인덱스는CREATE TABLE안에 인라인으로.- 함수·트리거는
NO SQL/READS SQL DATA선언이 필요하다. 바이너리 로깅이 켜진 서버에서만 터진다. - 버전은 타임스탬프. 정수 순번은 브랜치에서 충돌한다.
- 성공한
V는 불변,R__는 고치는 게 정상. - 파괴적 변경은 expand/contract 두 단계로.
'데이터베이스' 카테고리의 다른 글
| PostgreSQL 인덱스 재구성 시점 — 달력이 아니라 밀도로 정한다 (0) | 2026.09.21 |
|---|---|
| 튜닝하다 조건을 떨어뜨렸다 — 쿼리를 고치면 결과부터 대조한다 (0) | 2026.09.16 |
| MySQL 집계 쿼리를 요약 테이블로 빼면서 정확도를 안 버리는 법 (0) | 2026.09.13 |
| MySQL 타임존 설정, TIMESTAMP와 DATETIME이 다르게 움직인다 (0) | 2026.09.10 |
| 816건 넣으려고 16,268행을 잠갔다 — MySQL INSERT SELECT 락 실측 (0) | 2026.09.06 |
댓글