MySQL SQL mode 이해: strict mode, only_full_group_by, 날짜 처리 정책
MySQL SQL mode가 strict mode, only_full_group_by, 날짜 검증과 운영 안정성에 미치는 영향을 DBA 관점에서 정리한다.
1. 왜 SQL mode를 운영 기준으로 관리해야 하는가
MySQL의 sql_mode는 SQL 문법을 조금 엄격하게 해석할지, 잘못된 데이터를 경고로만 처리할지, 집계 쿼리에서 비결정적인 컬럼 참조를 허용할지, 날짜와 문자열 변환을 어느 수준까지 허용할지를 결정하는 서버 동작 정책이다. 같은 애플리케이션 코드와 같은 스키마라도 sql_mode 값이 다르면 INSERT가 성공하거나 실패할 수 있고, SELECT 결과가 달라질 수 있으며, 복제나 마이그레이션 과정에서 예상하지 못한 데이터 불일치가 발생할 수 있다.
운영 관점에서 sql_mode는 단순한 호환성 옵션이 아니다. 데이터 품질, 장애 조기 발견, 배포 안정성, 분석 쿼리의 결정성, 레거시 애플리케이션 호환성 사이의 균형을 정하는 핵심 정책이다. 특히 MySQL 5.7 이후 기본값이 점점 엄격해졌고, MySQL 8.0에서는 ONLY_FULL_GROUP_BY, STRICT_TRANS_TABLES, NO_ZERO_DATE, NO_ZERO_IN_DATE 등의 영향이 실무에서 자주 드러난다. Aurora MySQL도 호환 버전에 따라 기본 SQL mode와 파라미터 그룹 적용 방식이 달라지므로, 버전 업그레이드나 신규 클러스터 생성 시 반드시 확인해야 한다.
이 글은 SQL mode의 내부 적용 지점과 대표 모드의 의미를 설명하고, 운영자가 어떤 기준으로 설정·진단·변경해야 하는지 정리한다.
2. SQL mode의 적용 범위와 동작 방식
sql_mode는 전역 변수와 세션 변수로 존재한다.
GLOBAL sql_mode: 새로 생성되는 세션의 기본값이다.SESSION sql_mode: 현재 연결에서 실제로 SQL 실행에 적용되는 값이다.
이미 연결된 세션은 GLOBAL sql_mode가 바뀌어도 자동으로 변경되지 않는다. 커넥션 풀을 사용하는 애플리케이션에서는 이 차이가 중요하다. 운영자가 전역 값을 바꿨다고 해도, 기존 커넥션 풀의 세션은 이전 SESSION sql_mode를 계속 사용할 수 있다. 따라서 SQL mode 변경은 DB 파라미터 변경만이 아니라 애플리케이션 커넥션 재생성, 배포 순서, 세션 초기화 SQL까지 함께 검토해야 한다.
현재 설정은 다음과 같이 확인한다.
SELECT @@GLOBAL.sql_mode AS global_sql_mode,
@@SESSION.sql_mode AS session_sql_mode;
SHOW VARIABLES LIKE 'sql_mode';
세션 단위로 임시 변경할 때는 다음처럼 실행한다.
SET SESSION sql_mode = 'STRICT_TRANS_TABLES,ONLY_FULL_GROUP_BY,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';
전역 변경은 다음과 같이 가능하지만, 영구 설정은 설정 파일 또는 관리형 서비스의 파라미터 그룹에서 관리해야 한다.
SET GLOBAL sql_mode = 'STRICT_TRANS_TABLES,ONLY_FULL_GROUP_BY,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';
MySQL 8.0에서는 SET PERSIST를 통해 서버 재시작 후에도 유지되는 값을 기록할 수 있다.
SET PERSIST sql_mode = 'STRICT_TRANS_TABLES,ONLY_FULL_GROUP_BY,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';
다만 운영 표준에서는 DB 서버 안에서 임의로 SET PERSIST를 남기는 방식보다 설정 관리 체계, IaC, 파라미터 그룹, 변경 이력을 통해 관리하는 편이 추적성과 재현성이 좋다.
3. strict mode: 잘못된 데이터를 조용히 보정하지 않게 하는 장치
strict mode는 잘못된 값, 범위를 벗어난 값, 누락된 NOT NULL 값, 잘린 문자열 등을 MySQL이 자동 보정하거나 경고로만 넘기지 않고 오류로 처리하게 하는 정책이다. 대표적으로 STRICT_TRANS_TABLES와 STRICT_ALL_TABLES가 있다.
STRICT_TRANS_TABLES: 트랜잭션 스토리지 엔진에 대해 엄격하게 처리한다. InnoDB 중심 운영에서는 일반적으로 이 모드가 사용된다.STRICT_ALL_TABLES: 트랜잭션 엔진과 비트랜잭션 엔진 모두에 더 일관되게 엄격한 정책을 적용한다.
예를 들어 다음 테이블을 생각해 보자.
DROP TABLE IF EXISTS sql_mode_demo;
CREATE TABLE sql_mode_demo (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
code VARCHAR(5) NOT NULL,
amount INT NOT NULL,
created_on DATE NOT NULL
) ENGINE=InnoDB;
strict mode가 약하면 너무 긴 문자열이 잘리거나, 잘못된 날짜가 0000-00-00으로 들어가거나, 숫자 변환이 경고로 처리될 수 있다. 반면 strict mode에서는 다음과 같은 입력이 오류가 된다.
INSERT IGNORE INTO sql_mode_demo (code, amount, created_on)
VALUES ('TOO-LONG-CODE', 100, '2026-05-20'),
('A001', 'not-a-number', '2026-05-20'),
('A002', 100, '2026-02-31');
SHOW WARNINGS;
SELECT id, code, amount, created_on FROM sql_mode_demo ORDER BY id;
실행 결과(MySQL 8.0.46):
+---------+------+----------------------------------------------------------------------+
| Level | Code | Message |
+---------+------+----------------------------------------------------------------------+
| Warning | 1265 | Data truncated for column 'code' at row 1 |
| Warning | 1366 | Incorrect integer value: 'not-a-number' for column 'amount' at row 2 |
| Warning | 1264 | Out of range value for column 'created_on' at row 3 |
+---------+------+----------------------------------------------------------------------+
+----+-------+--------+------------+
| id | code | amount | created_on |
+----+-------+--------+------------+
| 1 | TOO-L | 100 | 2026-05-20 |
| 2 | A001 | 0 | 2026-05-20 |
| 3 | A002 | 100 | 0000-00-00 |
+----+-------+--------+------------+
운영적으로 strict mode는 장애를 더 많이 만드는 옵션처럼 보일 수 있지만, 실제로는 잘못된 데이터를 조기에 거부하여 장애 범위를 줄이는 장치다. 데이터가 한 번 잘못 저장되면 애플리케이션, 배치, 통계, 복제, 백업, 외부 연동까지 영향을 확산시킨다. 입력 시점의 오류는 배포나 요청 단위로 복구할 수 있지만, 누적된 데이터 품질 문제는 원인 추적과 정정 비용이 훨씬 크다.
3.1 strict mode에서 주의할 점
strict mode를 켠다고 모든 위험이 사라지는 것은 아니다. MySQL은 문맥에 따라 암시적 형 변환을 수행할 수 있고, 일부 SELECT 조건에서는 여전히 경고 기반 변환이 발생할 수 있다. 예를 들어 문자열 컬럼과 숫자를 비교할 때 인덱스 사용과 비교 결과가 예상과 달라질 수 있다.
-- 문자열 컬럼 code에 숫자를 비교하면 암시적 변환이 개입할 수 있다.
SELECT *
FROM sql_mode_demo
WHERE code = 100;
SHOW WARNINGS;
실행 결과(MySQL 8.0.46):
+---------+------+-------------------------------------------+
| Level | Code | Message |
+---------+------+-------------------------------------------+
| Warning | 1292 | Truncated incorrect DOUBLE value: 'TOO-L' |
| Warning | 1292 | Truncated incorrect DOUBLE value: 'A001' |
| Warning | 1292 | Truncated incorrect DOUBLE value: 'A002' |
+---------+------+-------------------------------------------+
따라서 strict mode는 최소 방어선이며, 애플리케이션의 타입 검증, 명시적 캐스팅, 스키마 제약, 테스트 데이터 품질 검사가 함께 필요하다.
4. ONLY_FULL_GROUP_BY: 비결정적인 집계 쿼리를 막는 정책
ONLY_FULL_GROUP_BY는 SELECT 목록, HAVING, ORDER BY에 등장하는 비집계 컬럼이 GROUP BY 컬럼에 함수적으로 종속되지 않으면 오류를 발생시키는 모드다. 이 모드가 꺼져 있으면 MySQL은 그룹 안의 여러 행 중 어느 행의 값을 보여줄지 명확하지 않은 컬럼도 허용할 수 있다. 결과는 실행 계획이나 데이터 상태에 따라 달라질 수 있으므로 운영 리포트와 정산 로직에서는 매우 위험하다.
예를 들어 다음 쿼리는 사용자별 주문 합계를 구하면서 주문 상태 하나를 함께 출력하려 한다.
DROP TABLE IF EXISTS orders;
CREATE TABLE orders (
order_id BIGINT PRIMARY KEY AUTO_INCREMENT,
customer_id BIGINT NOT NULL,
status VARCHAR(20) NOT NULL,
amount DECIMAL(10,2) NOT NULL,
created_at DATETIME NOT NULL,
KEY ix_customer_created (customer_id, created_at)
) ENGINE=InnoDB;
INSERT INTO orders (customer_id, status, amount, created_at) VALUES
(1001, 'PAID', 120.00, '2026-05-20 10:00:00'),
(1001, 'CANCELLED', 30.00, '2026-05-20 11:00:00'),
(1002, 'PAID', 50.00, '2026-05-20 12:00:00');
SELECT customer_id,
COUNT(DISTINCT status) AS status_count,
SUM(amount) AS total_amount
FROM orders
GROUP BY customer_id;
실행 결과(MySQL 8.0.46):
+-------------+--------------+--------------+
| customer_id | status_count | total_amount |
+-------------+--------------+--------------+
| 1001 | 2 | 150.00 |
| 1002 | 1 | 50.00 |
+-------------+--------------+--------------+
한 고객에게 PAID, CANCELLED, REFUNDED 상태의 주문이 섞여 있다면 status는 어떤 행의 값이어야 하는가? ONLY_FULL_GROUP_BY가 꺼져 있으면 MySQL이 임의의 값을 반환할 수 있고, 켜져 있으면 오류로 막는다.
올바른 쿼리는 의도를 명확히 해야 한다.
-- 상태별 합계를 원한다면 GROUP BY에 status를 포함한다.
SELECT customer_id,
status,
SUM(amount) AS total_amount
FROM orders
GROUP BY customer_id, status;
-- 고객별 총액만 원한다면 status를 제거한다.
SELECT customer_id,
SUM(amount) AS total_amount
FROM orders
GROUP BY customer_id;
-- 대표 상태를 특정 규칙으로 고른다면 집계 함수를 명시한다.
SELECT customer_id,
MAX(status) AS max_status,
SUM(amount) AS total_amount
FROM orders
GROUP BY customer_id;
실행 결과(MySQL 8.0.46):
+-------------+-----------+--------------+
| customer_id | status | total_amount |
+-------------+-----------+--------------+
| 1001 | PAID | 120.00 |
| 1001 | CANCELLED | 30.00 |
| 1002 | PAID | 50.00 |
+-------------+-----------+--------------+
+-------------+--------------+
| customer_id | total_amount |
+-------------+--------------+
| 1001 | 150.00 |
| 1002 | 50.00 |
+-------------+--------------+
+-------------+------------+--------------+
| customer_id | max_status | total_amount |
+-------------+------------+--------------+
| 1001 | PAID | 150.00 |
| 1002 | PAID | 50.00 |
+-------------+------------+--------------+
MySQL 5.7 이후에는 함수적 종속성(functional dependency)을 일부 인식한다. 예를 들어 기본키로 그룹화하면 같은 행의 다른 컬럼은 결정 가능하다고 판단할 수 있다.
DROP TABLE IF EXISTS customers;
CREATE TABLE customers (
customer_id BIGINT PRIMARY KEY,
email VARCHAR(255) NOT NULL,
name VARCHAR(100) NOT NULL
) ENGINE=InnoDB;
INSERT INTO customers VALUES (1001, 'kim@example.com', 'Kim');
SELECT customer_id, email, name
FROM customers
GROUP BY customer_id;
DROP TABLE customers;
실행 결과(MySQL 8.0.46):
+-------------+-----------------+------+
| customer_id | email | name |
+-------------+-----------------+------+
| 1001 | kim@example.com | Kim |
+-------------+-----------------+------+
customer_id가 기본키라면 email, name은 각 그룹에서 하나로 결정된다. 하지만 복잡한 조인, 표현식, 유니크 인덱스의 NULL 허용 여부, 파생 테이블에서는 기대와 다르게 오류가 날 수 있다. 이때는 쿼리 의도를 명시적으로 고치는 것이 가장 안전하다.
4.1 ANY_VALUE()는 마지막 수단이어야 한다
MySQL은 ONLY_FULL_GROUP_BY 오류를 피하기 위해 ANY_VALUE() 함수를 제공한다.
SELECT customer_id,
ANY_VALUE(status) AS sample_status,
SUM(amount) AS total_amount
FROM orders
GROUP BY customer_id;
실행 결과(MySQL 8.0.46):
+-------------+---------------+--------------+
| customer_id | sample_status | total_amount |
+-------------+---------------+--------------+
| 1001 | PAID | 150.00 |
| 1002 | PAID | 50.00 |
+-------------+---------------+--------------+
하지만 ANY_VALUE()는 “아무 값이나 괜찮다”는 뜻이다. 운영 리포트, 정산, 알림, 배치 조건에 사용하면 의사결정이 비결정적이 된다. 정말 샘플 값이 필요한 진단 쿼리나 임시 분석이 아니라면 MIN(), MAX(), GROUP_CONCAT(), 윈도우 함수, 서브쿼리로 명확한 규칙을 작성하는 편이 좋다.
5. 날짜 처리 정책: 0000-00-00과 잘못된 날짜를 어떻게 다룰 것인가
MySQL의 날짜 처리 정책은 레거시 시스템에서 특히 민감하다. 과거 MySQL 환경에서는 0000-00-00을 미정 날짜, 초기값, 외부 시스템 누락값의 대체값처럼 사용하는 경우가 많았다. 그러나 현대적인 운영 기준에서는 무효 날짜를 실제 날짜 타입에 저장하는 방식은 데이터 품질과 애플리케이션 호환성 측면에서 위험하다.
관련 SQL mode는 다음과 같다.
NO_ZERO_DATE:0000-00-00날짜 사용을 제한한다.NO_ZERO_IN_DATE:2026-00-15,2026-05-00처럼 월 또는 일이 0인 날짜를 제한한다.ALLOW_INVALID_DATES: 날짜의 월/일 유효성 검사를 완화한다. 일반 운영에서는 권장하지 않는다.
다음 입력은 날짜 정책에 따라 경고 또는 오류가 될 수 있다.
INSERT IGNORE INTO sql_mode_demo (code, amount, created_on)
VALUES ('D001', 10, '0000-00-00'),
('D002', 10, '2026-00-15'),
('D003', 10, '2026-02-31');
SHOW WARNINGS;
실행 결과(MySQL 8.0.46):
+---------+------+-----------------------------------------------------+
| Level | Code | Message |
+---------+------+-----------------------------------------------------+
| Warning | 1264 | Out of range value for column 'created_on' at row 3 |
+---------+------+-----------------------------------------------------+
운영 설계에서는 “모르는 날짜”와 “아직 정해지지 않은 날짜”를 날짜 타입의 무효값으로 표현하지 않는 것이 좋다. 보통 다음 중 하나를 선택한다.
- 값이 없을 수 있으면 컬럼을
NULL허용으로 설계하고 의미를 문서화한다. - 업무상 상태를 표현해야 한다면 별도 상태 컬럼을 둔다.
- 외부 원천 데이터의 품질 문제가 있다면 원천 문자열 컬럼과 정규화된 날짜 컬럼을 분리한다.
예를 들면 다음과 같다.
CREATE TABLE contract_event (
event_id BIGINT PRIMARY KEY AUTO_INCREMENT,
contract_id BIGINT NOT NULL,
effective_date DATE NULL,
effective_date_status ENUM('KNOWN', 'UNKNOWN', 'PENDING') NOT NULL,
source_effective_date VARCHAR(32) NULL,
CHECK (
(effective_date_status = 'KNOWN' AND effective_date IS NOT NULL)
OR
(effective_date_status <> 'KNOWN' AND effective_date IS NULL)
)
) ENGINE=InnoDB;
MySQL 8.0.16 이후에는 CHECK 제약이 실제로 적용된다. 그 이전 버전이나 일부 호환 환경에서는 애플리케이션 검증과 트리거, 배치 검증을 함께 고려해야 한다.
6. 운영 환경에서 SQL mode를 진단하는 방법
SQL mode 문제는 보통 신규 배포, 버전 업그레이드, 마이그레이션, 커넥션 풀 변경, ORM 업그레이드 시점에 드러난다. 운영자는 현재 서버 설정만 보지 말고 세션별 실제 적용 값과 경고 발생 여부를 함께 확인해야 한다.
6.1 현재 연결과 서버 기본값 확인
SELECT @@version AS mysql_version,
@@version_comment AS version_comment,
@@GLOBAL.sql_mode AS global_sql_mode,
@@SESSION.sql_mode AS session_sql_mode;
실행 결과(MySQL 8.0.46):
+---------------+------------------------------+------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------+
| mysql_version | version_comment | global_sql_mode | session_sql_mode |
+---------------+------------------------------+------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------+
| 8.0.46 | MySQL Community Server - GPL | ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION | ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION |
+---------------+------------------------------+------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------+
애플리케이션 계정으로 직접 접속해 확인하는 것이 중요하다. DBA 계정으로 접속한 세션과 애플리케이션 커넥션 풀 세션의 초기화 SQL이 다를 수 있기 때문이다.
6.2 경고를 오류처럼 관찰하기
strict mode가 아니거나 일부 문맥에서 경고가 발생하면 애플리케이션은 성공으로 인식하지만 데이터는 변형될 수 있다.
INSERT IGNORE INTO sql_mode_demo (code, amount, created_on)
VALUES ('WARN1', '123abc', '2026-05-20');
SHOW WARNINGS;
실행 결과(MySQL 8.0.46):
+---------+------+---------------------------------------------+
| Level | Code | Message |
+---------+------+---------------------------------------------+
| Warning | 1265 | Data truncated for column 'amount' at row 1 |
+---------+------+---------------------------------------------+
운영 점검에서는 배포 전 테스트에서 SHOW WARNINGS 또는 드라이버의 warning 수집 기능을 확인하고, 데이터 변환 경고가 없는지 점검하는 것이 좋다. 특히 ETL, 대량 적재, CSV import, 레거시 데이터 보정 작업은 경고를 무시하면 대량의 잘린 값이나 0 날짜를 만들 수 있다.
6.3 설정 파일과 파라미터 그룹 확인
일반 MySQL 서버에서는 설정 파일을 확인한다.
mysql --batch --skip-column-names -e "SELECT @@GLOBAL.sql_mode, @@SESSION.sql_mode;"
# 예시: my.cnf 또는 mysqld 설정에서 sql_mode 확인
mysqld --verbose --help 2>/dev/null | grep -i '^sql-mode'
컨테이너, Kubernetes, 자동화된 VM 환경에서는 이미지 기본값, ConfigMap, 환경 변수, 초기화 SQL, Helm values가 서로 다른 값을 만들 수 있다. 운영 표준은 “현재 DB에서 보이는 값”과 “재시작 후 적용될 선언적 설정”이 일치하는지 확인해야 한다.
7. Aurora MySQL에서의 해석
Aurora MySQL은 MySQL 호환 엔진이지만 설정 적용 방식은 자체 운영 모델을 따른다. sql_mode는 DB cluster parameter group 또는 DB parameter group의 영향을 받으며, 일부 파라미터는 동적 적용이 가능하고 일부는 재부팅 또는 연결 재생성이 필요할 수 있다. 실제 적용 여부는 AWS 콘솔의 파라미터 값만으로 판단하지 말고 DB 세션에서 @@GLOBAL.sql_mode, @@SESSION.sql_mode를 확인해야 한다.
Aurora 운영에서 특히 주의할 점은 다음과 같다.
-
클러스터 복제본과 writer 세션의 일관성
읽기 복제본으로 연결되는 애플리케이션 세션도 같은 SQL mode를 사용하는지 확인해야 한다. 읽기 쿼리의ONLY_FULL_GROUP_BY오류는 writer가 아니라 reader endpoint에서 먼저 드러날 수 있다. -
버전 업그레이드 전 호환성 점검
Aurora MySQL 5.7 호환에서 8.0 호환으로 이동할 때 기본 SQL mode, 예약어, 함수적 종속성 판단, 날짜 검증, CHECK 제약 등 여러 요소가 함께 바뀐다. SQL mode 차이만 따로 보지 말고 애플리케이션 쿼리 회귀 테스트를 같이 수행해야 한다. -
파라미터 그룹 변경 후 기존 커넥션 처리
전역 값이 바뀌어도 기존 애플리케이션 세션은 이전 세션 값을 유지할 수 있다. RDS Proxy, 애플리케이션 커넥션 풀, 장기 실행 배치 연결을 사용하는 경우 재연결 전략이 필요하다. -
Performance Insights와 오류 로그의 보조 활용
SQL mode 변경 후 특정 SQL digest의 오류율이 증가하거나 지연이 늘 수 있다. Aurora에서는 Performance Insights, CloudWatch Logs, 애플리케이션 오류 로그를 함께 비교해 변경 영향을 확인하는 것이 좋다.
8. 변경 전략: 끄기보다 고치기, 한 번에 바꾸기보다 단계적으로 검증하기
운영 중 ONLY_FULL_GROUP_BY 오류나 strict mode 오류가 발생하면 가장 쉬운 대응은 SQL mode를 완화하는 것이다. 그러나 장기적으로는 대개 좋은 선택이 아니다. SQL mode를 완화하면 장애는 사라진 것처럼 보이지만 비결정적 결과, 잘린 데이터, 무효 날짜가 다시 허용된다.
권장 접근은 다음 순서다.
- 현재 운영 값과 목표 표준 값을 문서화한다.
- 스테이징 또는 읽기 전용 복제 환경에서 목표 SQL mode를 적용한다.
- 애플리케이션 통합 테스트와 주요 배치 SQL을 실행한다.
- 오류 쿼리를 분류한다.
- 집계 쿼리 오류: GROUP BY, 집계 함수, 윈도우 함수, 서브쿼리로 의도 명확화
- 입력 오류: 애플리케이션 검증, 스키마 기본값, NULL 정책, 날짜 정책 수정
- 레거시 데이터 오류: 데이터 정제 후 제약 강화
- 변경 후 새 커넥션이 목표 SQL mode를 사용하도록 배포 순서를 조정한다.
- 운영 반영 후 경고·오류·데이터 품질 지표를 관찰한다.
예를 들어 ONLY_FULL_GROUP_BY 오류는 다음처럼 고칠 수 있다.
-- 문제 쿼리가 모호해질 수 있는 고객을 먼저 찾는다.
SELECT customer_id,
COUNT(DISTINCT status) AS status_count,
MAX(created_at) AS last_order_at
FROM orders
GROUP BY customer_id
HAVING COUNT(DISTINCT status) > 1;
-- 개선 예시: 최신 주문 행을 먼저 구한 뒤 조인한다.
WITH last_order AS (
SELECT customer_id, MAX(created_at) AS last_order_at
FROM orders
GROUP BY customer_id
)
SELECT o.customer_id,
o.status,
o.created_at AS last_order_at
FROM orders o
JOIN last_order lo
ON lo.customer_id = o.customer_id
AND lo.last_order_at = o.created_at;
실행 결과(MySQL 8.0.46):
+-------------+--------------+---------------------+
| customer_id | status_count | last_order_at |
+-------------+--------------+---------------------+
| 1001 | 2 | 2026-05-20 11:00:00 |
+-------------+--------------+---------------------+
+-------------+-----------+---------------------+
| customer_id | status | last_order_at |
+-------------+-----------+---------------------+
| 1001 | CANCELLED | 2026-05-20 11:00:00 |
| 1002 | PAID | 2026-05-20 12:00:00 |
+-------------+-----------+---------------------+
동일 시각 주문이 여러 개일 수 있다면 기본키를 포함한 추가 규칙을 넣어야 한다. 핵심은 SQL mode 오류를 “귀찮은 문법 제약”으로 보지 말고, 업무 규칙이 SQL에 충분히 표현되지 않았다는 신호로 해석하는 것이다.
9. 자주 발생하는 오해와 장애 패턴
9.1 “개발에서는 되는데 운영에서 실패한다”
개발 DB와 운영 DB의 sql_mode가 다르면 동일 SQL이 다르게 동작한다. 로컬 Docker 이미지의 기본값, ORM 테스트 DB, CI DB, 운영 Aurora 파라미터 그룹이 서로 다를 수 있다. 개발 환경을 운영보다 느슨하게 두면 오류가 운영에서 늦게 발견된다.
9.2 “GLOBAL 값을 바꿨는데 애플리케이션 오류가 계속된다”
기존 세션은 기존 SESSION sql_mode를 유지한다. 커넥션 풀의 세션을 재생성하거나 애플리케이션을 재시작해야 할 수 있다. 일부 프레임워크는 연결 직후 자체적으로 SET sql_mode를 실행하기도 하므로 애플리케이션 설정도 확인해야 한다.
9.3 “ANY_VALUE()로 고치면 된다”
ANY_VALUE()는 오류를 없애지만 업무 의미를 보장하지 않는다. 정산, 과금, 재고, 권한, 알림 대상 선정 같은 영역에서는 사용을 피해야 한다. 임의 값이 허용되는 진단용 쿼리인지 먼저 확인해야 한다.
9.4 “0 날짜는 편리한 기본값이다”
0000-00-00은 날짜가 아니다. 외부 시스템과 연동하거나 JSON, Java, Python, BI 도구로 데이터를 전달할 때 파싱 오류와 예외 처리를 만들 수 있다. 미정 상태는 NULL 또는 상태 컬럼으로 표현하는 편이 안전하다.
9.5 “SQL mode는 성능과 무관하다”
대부분의 SQL mode는 직접적인 성능 튜닝 옵션이 아니다. 그러나 비결정적 GROUP BY를 명확한 조인·윈도우 함수로 고치는 과정에서 실행 계획이 바뀔 수 있고, 암시적 형 변환을 제거하면 인덱스 사용성이 좋아질 수 있다. 또한 데이터 품질이 좋아지면 배치 보정과 예외 처리 비용이 줄어든다.
10. 운영 표준 예시
새로운 MySQL 8.0 또는 Aurora MySQL 3 계열 환경에서는 다음과 같은 방향을 기본값으로 검토할 수 있다. 실제 값은 애플리케이션 호환성과 조직 표준에 맞게 조정해야 한다.
STRICT_TRANS_TABLES,
ONLY_FULL_GROUP_BY,
ERROR_FOR_DIVISION_BY_ZERO,
NO_ENGINE_SUBSTITUTION
날짜 관련 모드는 버전별 기본값과 deprecated 여부가 있으므로 사용 중인 MySQL 버전에서 실제 동작을 확인해야 한다. 중요한 것은 “무효 날짜를 허용하지 않는다”는 정책을 테스트와 스키마 설계로 보장하는 것이다.
운영 점검용 쿼리는 다음과 같이 준비할 수 있다.
-- 서버와 세션 SQL mode 확인
SELECT @@hostname AS host_name,
@@port AS port,
@@version AS version,
@@GLOBAL.sql_mode AS global_sql_mode,
@@SESSION.sql_mode AS session_sql_mode;
-- 현재 세션에서 특정 모드 포함 여부 확인
SELECT FIND_IN_SET('STRICT_TRANS_TABLES', @@SESSION.sql_mode) AS has_strict_trans_tables,
FIND_IN_SET('ONLY_FULL_GROUP_BY', @@SESSION.sql_mode) AS has_only_full_group_by,
FIND_IN_SET('NO_ENGINE_SUBSTITUTION', @@SESSION.sql_mode) AS has_no_engine_substitution;
-- 날짜 품질 점검 예시: 실제 테이블명과 컬럼명에 맞게 수정
DROP TABLE IF EXISTS some_table;
CREATE TABLE some_table (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
date_col DATE NULL
) ENGINE=InnoDB;
SELECT COUNT(*) AS zero_date_count
FROM some_table
WHERE date_col = '0000-00-00';
DROP TABLE some_table;
DROP TABLE orders;
DROP TABLE sql_mode_demo;
실행 결과(MySQL 8.0.46):
+-----------+------+---------+-----------------------------------------------------------------------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------+
| host_name | port | version | global_sql_mode | session_sql_mode |
+-----------+------+---------+-----------------------------------------------------------------------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------+
| MySQL | 3306 | 8.0.40 | ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION | ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION |
+-----------+------+---------+-----------------------------------------------------------------------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------+
+-------------------------+------------------------+----------------------------+
| has_strict_trans_tables | has_only_full_group_by | has_no_engine_substitution |
+-------------------------+------------------------+----------------------------+
| 2 | 1 | 6 |
+-------------------------+------------------------+----------------------------+
+-----------------+
| zero_date_count |
+-----------------+
| 0 |
+-----------------+
마지막 쿼리는 예시다. 운영 테이블에 직접 적용하기 전에는 테이블 크기, 인덱스, 읽기 부하, 실행 계획을 확인해야 한다.
11. DBA 체크리스트
- 운영, 스테이징, 개발, CI 환경의
@@GLOBAL.sql_mode와@@SESSION.sql_mode - 애플리케이션 커넥션 풀 또는 ORM이 연결 직후
SET sql_mode -
STRICT_TRANS_TABLES -
ONLY_FULL_GROUP_BY -
0000-00-00
12. 결론
MySQL SQL mode는 데이터베이스의 관용 수준을 정하는 운영 정책이다. strict mode는 잘못된 데이터를 조기에 거부하고, ONLY_FULL_GROUP_BY는 비결정적인 집계 결과를 막으며, 날짜 처리 정책은 장기적인 데이터 호환성과 품질을 좌우한다. SQL mode 오류를 단순히 “버전이 올라가서 생긴 불편”으로 보면 설정을 완화하는 방향으로 흐르기 쉽다. 그러나 안정적인 운영 표준에서는 오류가 드러난 지점에서 데이터 모델, 입력 검증, 쿼리 의미를 명확히 고치는 편이 장기 비용을 줄인다.
다음 단계에서는 SQL mode와 함께 자주 점검해야 하는 문자셋, collation, time zone 같은 세션·서버 기본 정책을 함께 다루면 MySQL 운영 환경의 재현성과 예측 가능성을 더 높일 수 있다.