카테고리 : MySQL/기술노트

시계열 테이블 파티션 운영: 월별 파티션 생성·삭제 자동화

MySQL 월별 RANGE 파티션의 선행 생성, MAXVALUE 분할, 재실행 안전성, 보관 경계와 삭제 승인을 실제 SQL로 검증한다.

저자: MySQL 기술 노트 작성: 2026.10.20 약 9분 5,263자
다운로드

월별 파티션 운영의 목표는 매월 DDL을 실행하는 것이 아니라, 앞으로 들어올 데이터를 받을 범위와 보존해야 할 과거 범위를 계속 유지하는 것이다. 작업 이력에 성공이 찍혀 있어도 미래 파티션이 부족하거나, 잘못 계산한 보관 경계로 아직 필요한 데이터를 삭제했다면 자동화는 실패한 것이다.

이 글은 RANGE COLUMNS(occurred_on)을 사용하는 InnoDB 테이블을 대상으로 생성과 삭제를 서로 다른 위험 등급으로 분리한다. 재실행 가능한 생성 절차와 삭제 후보 조회를 실제로 실행하고, 오래된 파티션을 삭제한 뒤 과거 데이터가 다시 들어오는 경우까지 확인한다. SQL은 빈 실습 스키마에서 위에서 아래로 실행한다. 운영 테이블에 그대로 적용하는 범용 관리 프로그램이 아니며, 특히 삭제 예제는 실습 데이터에만 사용한다.

1. 월 이름보다 중요한 것은 배타적 상한이다

p202610 VALUES LESS THAN ('2026-11-01')의 이름은 사람이 붙인 표식이다. 실제 배치 기준은 파티션 정의의 상한과 바로 앞 파티션의 상한이다. 앞 경계가 2026-10-01이라면 이 파티션의 범위는 [2026-10-01, 2026-11-01)이다. 날짜를 BETWEEN의 양 끝 포함 조건으로 표현하면 다음 달 첫날을 중복 포함하기 쉬우므로, 보관 정책과 조회 조건 모두 반개방 구간으로 정리한다.

첫 파티션에는 명시적인 하한이 없다. p202608이라는 이름을 붙여도 상한보다 작은 과거 날짜가 함께 들어갈 수 있다. NOT NULL은 NULL만 차단하며, 지나치게 오래되거나 먼 미래의 날짜를 차단하지 않는다. 입력 허용 기간은 애플리케이션 또는 별도의 검증 계층이 책임져야 한다.

월 경계의 시간대도 데이터 계약이다. 예제는 DATE이므로 시간대 변환을 하지 않는다. UTC DATETIME에 한국 업무 월을 적용한다면 업무 월 첫날을 UTC 경계로 변환하는 규칙이 필요하다. 실행 서버의 현지 날짜와 DB 세션의 날짜를 혼용하지 않는다. 월을 더할 때는 매월 1일에서 다음 월을 계산하며, 30일을 더하는 방식으로 대체하지 않는다.

2. 미래 범위를 비워 둘 것인가, MAXVALUE로 받을 것인가

미래 파티션을 미리 만들고 마지막에 MAXVALUE를 두지 않으면, 준비된 범위를 넘는 입력은 파티션을 찾지 못해 실패한다. 오류가 즉시 드러나는 대신 자동 생성 실패가 쓰기 장애로 연결된다. 이 방식에서는 마지막보다 큰 범위를 ADD PARTITION으로 붙일 수 있다.

마지막에 p_future VALUES LESS THAN (MAXVALUE)를 두면 아직 월별로 분리되지 않은 날짜도 받을 수 있다. 그러나 MAXVALUE 뒤에 더 큰 범위를 추가할 수는 없다. p_future를 월별 파티션과 새 p_future로 나누는 REORGANIZE PARTITION이 필요하다. 그 안에 이미 들어온 행은 새 경계에 맞춰 재배치된다.

중요한 비용 차이가 있다. MySQL 8.0의 InnoDB 파티션 DDL 지원표에서 REORGANIZE PARTITION은 INPLACE가 가능하지만 동시 DML을 허용하는 작업은 아니다. INPLACE는 쓰기 무중단의 동의어가 아니다. 비어 있는 미래 파티션을 선제적으로 분할하면 이동할 데이터는 줄지만, 잠금 획득과 짧은 쓰기 차단 가능성까지 사라지는 것은 아니다. 이미 커진 p_future를 바쁜 시간대에 분할하는 작업은 정기 유지보수보다 장애 수습에 가깝다.

flowchart TD
    A[정기 실행: 기준 월과 목표 경계 고정] --> B[단일 writer 및 중복 실행 차단 확인]
    B --> C[실제 파티션 이름과 경계 조회]
    C --> D{미래 월별 범위가 충분한가}
    D -->|아니오| E[다음 한 달만 분할하고 재조회]
    E --> C
    D -->|예| F[생성 단계 완료 기록]
    F --> G[보관 경계 이하의 삭제 후보 산출]
    G --> H{보존 예외와 복구 검증 및 승인 충족}
    H -->|아니오| I[삭제 보류 및 알림]
    H -->|예| J[한 파티션 삭제 후 경계와 데이터 확인]

3. 실습 데이터: 미래 파티션에 이미 행이 들어간 상태

업무 식별자와 파티션 키를 함께 기본키에 포함한다. 이 키는 두 컬럼 조합의 고유성을 보장하지만 id 단독의 전역 고유성을 보장하지 않는다. 파티션 설계 때문에 애플리케이션의 원래 고유성 요구를 약화시키지 않았는지 별도로 점검해야 한다.

CREATE TABLE monthly_events (
  id BIGINT NOT NULL,
  occurred_on DATE NOT NULL,
  amount INT NOT NULL,
  PRIMARY KEY (id, occurred_on)
) ENGINE=InnoDB
PARTITION BY RANGE COLUMNS (occurred_on) (
  PARTITION p202608 VALUES LESS THAN ('2026-09-01'),
  PARTITION p202609 VALUES LESS THAN ('2026-10-01'),
  PARTITION p_future VALUES LESS THAN (MAXVALUE)
);
INSERT INTO monthly_events VALUES
  (1, '2026-08-15', 100),
  (2, '2026-09-15', 200),
  (3, '2026-10-15', 300),
  (4, '2026-11-15', 400);
SELECT id, occurred_on, amount
FROM monthly_events PARTITION (p_future) ORDER BY id;

실행 결과(MySQL 8.0.x):

mysql> CREATE TABLE monthly_events (
    ->   id BIGINT NOT NULL,
    ->   occurred_on DATE NOT NULL,
    ->   amount INT NOT NULL,
    ->   PRIMARY KEY (id, occurred_on)
    -> ) ENGINE=InnoDB
    -> PARTITION BY RANGE COLUMNS (occurred_on) (
    ->   PARTITION p202608 VALUES LESS THAN ('2026-09-01'),
    ->   PARTITION p202609 VALUES LESS THAN ('2026-10-01'),
    ->   PARTITION p_future VALUES LESS THAN (MAXVALUE)
    -> );

Query OK, 0 rows affected (0.01 sec)

mysql> INSERT INTO monthly_events VALUES
    ->   (1, '2026-08-15', 100),
    ->   (2, '2026-09-15', 200),
    ->   (3, '2026-10-15', 300),
    ->   (4, '2026-11-15', 400);

Query OK, 4 rows affected (0.01 sec)
Records: 4  Duplicates: 0  Warnings: 0

mysql> SELECT id, occurred_on, amount
    -> FROM monthly_events PARTITION (p_future) ORDER BY id;

+----+-------------+--------+
| id | occurred_on | amount |
+----+-------------+--------+
|  3 | 2026-10-15  |    300 |
|  4 | 2026-11-15  |    400 |
+----+-------------+--------+
2 rows in set (0.00 sec)

생성 전 p_future에는 10월과 11월 행이 함께 들어 있다. 아래의 생성 절차는 이 데이터가 사라지지 않고 해당 월로 이동하는지도 확인한다. 실제 운영에서는 information_schema.PARTITIONS.TABLE_ROWS가 InnoDB 추정치라는 점에 주의한다. 0이라는 추정치만 보고 비어 있다고 판정하지 말고, 필요할 때 해당 파티션에 대한 LIMIT 1 조회로 존재 여부를 확인한다. 정확한 전체 행 수 계산은 별도의 읽기 부하를 만든다.

4. 생성 자동화의 핵심: 같은 이름이 아니라 같은 경계인가

다음 프로시저는 실습 테이블 전용이다. 전달한 월의 파티션이 있으면 상한까지 일치하는지 확인하고 종료한다. 없으면 현재 마지막 유한 상한이 요청 월의 첫날인지 확인한 뒤, 바로 다음 한 달만 만든다. 중간 월을 건너뛰는 요청을 거부하므로, 자동 실행기는 부족한 월을 오래된 순서부터 호출해야 한다.

GET_LOCK()은 같은 이름을 사용하는 관리 작업끼리만 협력적으로 직렬화한다. 일반 DML이나 수동 ALTER를 막는 잠금이 아니며 다른 서버 사이의 분산 잠금도 아니다. 예제의 잠금 이름은 실습용으로 고정했다. 실제 실행기는 대상 스키마·테이블을 구분하고, 생성과 삭제 모두 같은 관리 잠금에 참여하도록 해야 한다. 수동 스키마 변경과는 별도 변경 승인 절차로 충돌을 방지한다.

DELIMITER //
CREATE PROCEDURE ensure_event_month(IN month_start DATE)
SQL SECURITY INVOKER
main: BEGIN
  DECLARE part_name VARCHAR(16);
  DECLARE upper_text VARCHAR(32);
  DECLARE existing_text VARCHAR(32);
  DECLARE last_text VARCHAR(32);
  DECLARE part_count INT DEFAULT 0;
  DECLARE future_count INT DEFAULT 0;
  DECLARE got_lock INT DEFAULT 0;
  DECLARE EXIT HANDLER FOR SQLEXCEPTION
  BEGIN
    IF got_lock = 1 THEN
      DO RELEASE_LOCK('monthly_events_partition_maintenance');
    END IF;
    RESIGNAL;
  END;

  IF month_start IS NULL OR DAYOFMONTH(month_start) <> 1
     OR month_start < '2000-01-01' OR month_start >= '2100-01-01' THEN
    SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'invalid month start';
  END IF;
  SET got_lock = GET_LOCK('monthly_events_partition_maintenance', 0);
  IF got_lock IS NULL OR got_lock <> 1 THEN
    SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'maintenance lock unavailable';
  END IF;
  SET part_name = DATE_FORMAT(month_start, 'p%Y%m');
  SET upper_text = QUOTE(DATE_FORMAT(month_start + INTERVAL 1 MONTH, '%Y-%m-%d'));

  SELECT COUNT(*), MAX(PARTITION_DESCRIPTION)
    INTO part_count, existing_text
  FROM information_schema.PARTITIONS
  WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'monthly_events'
    AND PARTITION_NAME = part_name;
  IF part_count = 1 THEN
    IF NOT (existing_text <=> upper_text) THEN
      SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'partition boundary mismatch';
    END IF;
    SELECT part_name AS partition_name, 'already_present' AS action;
  ELSE
    SELECT COUNT(*) INTO future_count
    FROM information_schema.PARTITIONS
    WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'monthly_events'
      AND PARTITION_NAME = 'p_future' AND PARTITION_DESCRIPTION = 'MAXVALUE';
    SELECT MAX(PARTITION_DESCRIPTION) INTO last_text
    FROM information_schema.PARTITIONS
    WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'monthly_events'
      AND PARTITION_DESCRIPTION <> 'MAXVALUE';
    IF future_count <> 1
       OR NOT (last_text <=> QUOTE(DATE_FORMAT(month_start, '%Y-%m-%d'))) THEN
      SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'unexpected preceding boundary';
    END IF;
    SET @partition_ddl = CONCAT(
      'ALTER TABLE monthly_events ALGORITHM=INPLACE, LOCK=SHARED, REORGANIZE PARTITION p_future INTO (PARTITION ',
      part_name, ' VALUES LESS THAN (', upper_text,
      '), PARTITION p_future VALUES LESS THAN (MAXVALUE))');
    PREPARE monthly_stmt FROM @partition_ddl;
    EXECUTE monthly_stmt;
    DEALLOCATE PREPARE monthly_stmt;
    SELECT part_name AS partition_name, 'created' AS action;
  END IF;
  DO RELEASE_LOCK('monthly_events_partition_maintenance');
END//
DELIMITER ;
SET SESSION lock_wait_timeout = 5;
CALL ensure_event_month('2026-10-01');
CALL ensure_event_month('2026-10-01');
CALL ensure_event_month('2026-11-01');
SELECT PARTITION_NAME, PARTITION_DESCRIPTION
FROM information_schema.PARTITIONS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'monthly_events'
ORDER BY PARTITION_ORDINAL_POSITION;
SELECT 'october' AS bucket, COUNT(*) AS rows_found, SUM(amount) AS amount_sum
FROM monthly_events PARTITION (p202610)
UNION ALL
SELECT 'november', COUNT(*), SUM(amount)
FROM monthly_events PARTITION (p202611)
UNION ALL
SELECT 'future', COUNT(*), SUM(amount)
FROM monthly_events PARTITION (p_future);

실행 결과(MySQL 8.0.x):

프로시저 생성문과 긴 조회문은 줄이고, 호출 결과·파티션 경계·집계 표를 발췌했다. 실제 검증 버전은 MySQL 8.0.46이다.

mysql> CREATE PROCEDURE ensure_event_month ...;
Query OK, 0 rows affected (0.00 sec)

mysql> CALL ensure_event_month('2026-10-01');
+----------------+---------+
| partition_name | action  |
+----------------+---------+
| p202610        | created |
+----------------+---------+
1 row in set (0.01 sec)
Query OK, 0 rows affected (0.01 sec)

mysql> CALL ensure_event_month('2026-10-01');
+----------------+-----------------+
| partition_name | action          |
+----------------+-----------------+
| p202610        | already_present |
+----------------+-----------------+
1 row in set (0.00 sec)
Query OK, 0 rows affected (0.00 sec)

mysql> CALL ensure_event_month('2026-11-01');
+----------------+---------+
| partition_name | action  |
+----------------+---------+
| p202611        | created |
+----------------+---------+
1 row in set (0.01 sec)
Query OK, 0 rows affected (0.01 sec)

mysql> SELECT PARTITION_NAME, PARTITION_DESCRIPTION ...;
+----------------+-----------------------+
| PARTITION_NAME | PARTITION_DESCRIPTION |
+----------------+-----------------------+
| p202608        | '2026-09-01'          |
| p202609        | '2026-10-01'          |
| p202610        | '2026-11-01'          |
| p202611        | '2026-12-01'          |
| p_future       | MAXVALUE              |
+----------------+-----------------------+
5 rows in set (0.00 sec)

mysql> SELECT ... UNION ALL SELECT ... UNION ALL SELECT ...;
+----------+------------+------------+
| bucket   | rows_found | amount_sum |
+----------+------------+------------+
| october  |          1 |        300 |
| november |          1 |        400 |
| future   |          0 |       NULL |
+----------+------------+------------+
3 rows in set (0.00 sec)

같은 10월 요청의 두 번째 호출은 already_present를 반환하고, 11월 요청까지 끝나면 각 월의 기존 행이 해당 파티션에 배치된다. p_future의 행 수는 0이며 합계가 NULL인 것은 합산할 행이 없기 때문이다.

이 절차에서 MAX(PARTITION_DESCRIPTION)으로 경계를 비교할 수 있는 이유는 이 테이블의 단일 DATE 파티션 상한이 일정한 ISO 날짜 문자열로 표현되기 때문이다. 숫자 식, 복합 RANGE COLUMNS, 서브파티션으로 일반화하면 안 된다. 이미 있는 월에 대해서는 이름과 상한만 확인하므로, 실제 운영기의 사전 점검에서는 바로 앞 상한까지 포함한 전체 월별 구성이 기대와 일치하는지도 확인해야 한다. 식별자는 유효성 검사를 통과한 날짜에서만 생성하고 테이블 이름은 고정하여, 외부 문자열이 DDL에 그대로 삽입되지 않도록 했다.

5. 경계를 건너뛰는 요청은 실패로 처리한다

11월까지 만든 상태에서 12월을 건너뛰고 다음 해 1월을 요청하면, 실제 마지막 유한 경계와 요청 월이 일치하지 않는다. 아래의 검사용 프로시저는 예상된 오류만 포착하여 거부 여부를 보여 준다. 운영 실행기는 이런 오류를 성공으로 덮지 말고 실패 상태와 원인을 기록해야 한다.

DELIMITER //
CREATE PROCEDURE probe_month_gap()
BEGIN
  DECLARE rejected INT DEFAULT 0;
  DECLARE message_text_value TEXT DEFAULT '';
  DECLARE CONTINUE HANDLER FOR SQLSTATE '45000'
  BEGIN
    GET STACKED DIAGNOSTICS CONDITION 1 message_text_value = MESSAGE_TEXT;
    SET rejected = 1;
  END;
  CALL ensure_event_month('2027-01-01');
  SELECT rejected AS gap_rejected, message_text_value AS reason;
END//
DELIMITER ;
CALL probe_month_gap();
SELECT IS_FREE_LOCK('monthly_events_partition_maintenance') AS lock_released;
DROP PROCEDURE probe_month_gap;

실행 결과(MySQL 8.0.x):

mysql> CREATE PROCEDURE probe_month_gap()
    -> BEGIN
    ->   DECLARE rejected INT DEFAULT 0;
    ->   DECLARE message_text_value TEXT DEFAULT '';
    ->   DECLARE CONTINUE HANDLER FOR SQLSTATE '45000'
    ->   BEGIN
    ->     GET STACKED DIAGNOSTICS CONDITION 1 message_text_value = MESSAGE_TEXT;
    ->     SET rejected = 1;
    ->   END;
    ->   CALL ensure_event_month('2027-01-01');
    ->   SELECT rejected AS gap_rejected, message_text_value AS reason;
    -> END;

Query OK, 0 rows affected (0.00 sec)

mysql> CALL probe_month_gap();

+--------------+-------------------------------+
| gap_rejected | reason                        |
+--------------+-------------------------------+
|            1 | unexpected preceding boundary |
+--------------+-------------------------------+
1 row in set (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

mysql> SELECT IS_FREE_LOCK('monthly_events_partition_maintenance') AS lock_released;

+---------------+
| lock_released |
+---------------+
|             1 |
+---------------+
1 row in set (0.00 sec)

mysql> DROP PROCEDURE probe_month_gap;

Query OK, 0 rows affected (0.01 sec)

6. 삭제 자동화는 후보 계산과 실행 승인을 분리한다

예제의 보관 경계는 고정된 2026-09-01이다. 그보다 이른 날짜만 삭제할 수 있다고 가정한다. 파티션의 배타적 상한이 보관 경계 이하이면 전체 범위가 삭제 대상이 된다. 이름의 월만 비교하거나, 상한을 포함하는 날짜로 오해해서는 안 된다. 이 규칙은 중간 파티션이 모두 올바른 월 경계로 구성되어 있다는 점검을 대체하지 않는다.

다음 조회는 후보를 보여 줄 뿐, 삭제를 허가하지 않는다. 보존 예외, 감사·법적 보류, 아카이브 완료 및 복구 검증, 해당 월을 참조하는 배치의 종료를 별도 시스템에서 확인해야 한다. 부분 월 보관이 필요하다면 해당 파티션 전체를 삭제할 수 없으며, 행 단위 정책이나 더 작은 파티션 단위를 검토한다.

SET @retain_from = DATE('2026-09-01');
SELECT PARTITION_NAME,
       REPLACE(PARTITION_DESCRIPTION, CHAR(39), '') AS exclusive_upper
FROM information_schema.PARTITIONS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'monthly_events'
  AND PARTITION_DESCRIPTION <> 'MAXVALUE'
  AND CAST(REPLACE(PARTITION_DESCRIPTION, CHAR(39), '') AS DATE) <= @retain_from
ORDER BY PARTITION_ORDINAL_POSITION;
ALTER TABLE monthly_events ALGORITHM=INPLACE, LOCK=NONE, DROP PARTITION p202608;
SELECT id, occurred_on, amount FROM monthly_events ORDER BY id;
INSERT INTO monthly_events VALUES (5, '2026-08-20', 500);
SELECT id, occurred_on, amount
FROM monthly_events PARTITION (p202609) ORDER BY id;

실행 결과(MySQL 8.0.x):

mysql> SET @retain_from = DATE('2026-09-01');

Query OK, 0 rows affected (0.00 sec)

mysql> SELECT PARTITION_NAME,
    ->        REPLACE(PARTITION_DESCRIPTION, CHAR(39), '') AS exclusive_upper
    -> FROM information_schema.PARTITIONS
    -> WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'monthly_events'
    ->   AND PARTITION_DESCRIPTION <> 'MAXVALUE'
    ->   AND CAST(REPLACE(PARTITION_DESCRIPTION, CHAR(39), '') AS DATE) <= @retain_from
    -> ORDER BY PARTITION_ORDINAL_POSITION;

+----------------+-----------------+
| PARTITION_NAME | exclusive_upper |
+----------------+-----------------+
| p202608        | 2026-09-01      |
+----------------+-----------------+
1 row in set (0.00 sec)

mysql> ALTER TABLE monthly_events ALGORITHM=INPLACE, LOCK=NONE, DROP PARTITION p202608;

Query OK, 0 rows affected (0.01 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> SELECT id, occurred_on, amount FROM monthly_events ORDER BY id;

+----+-------------+--------+
| id | occurred_on | amount |
+----+-------------+--------+
|  2 | 2026-09-15  |    200 |
|  3 | 2026-10-15  |    300 |
|  4 | 2026-11-15  |    400 |
+----+-------------+--------+
3 rows in set (0.00 sec)

mysql> INSERT INTO monthly_events VALUES (5, '2026-08-20', 500);

Query OK, 1 row affected (0.00 sec)

mysql> SELECT id, occurred_on, amount
    -> FROM monthly_events PARTITION (p202609) ORDER BY id;

+----+-------------+--------+
| id | occurred_on | amount |
+----+-------------+--------+
|  2 | 2026-09-15  |    200 |
|  5 | 2026-08-20  |    500 |
+----+-------------+--------+
2 rows in set (0.00 sec)

ALGORITHM=INPLACE를 명시한 삭제는 해당 파티션의 데이터를 제거한다. 예제에서는 8월의 기존 행이 사라지고 9~11월 행이 유지된다. 그런데 삭제 뒤 새로 넣은 8월 행은 p202609에 들어간다. 이제 첫 파티션인 p202609가 2026-10-01보다 작은 모든 값을 받기 때문이다. 파티션 삭제는 과거 날짜 입력을 영구 차단하는 조치가 아니다. 지연 도착 데이터, 재전송, 백필을 어디까지 허용할지 먼저 정해야 보관 정책이 다시 깨지지 않는다.

알고리즘도 삭제 의미의 일부다. 공식 문서는 DROP PARTITION을 COPY로 수행하는 경우 재구축하면서 호환 범위의 다른 파티션으로 데이터를 옮길 수 있다고 구분한다. 따라서 예제는 INPLACE를 명시하고, 지원되지 않으면 실패하도록 한다. LOCK=NONE 역시 MDL을 전혀 얻지 않는다는 뜻은 아니다. DDL 전후 메타데이터 잠금과 장기 트랜잭션의 간섭은 별도로 고려해야 한다.

7. 정기 실행기를 운영 절차로 완성하기

생성 주기는 월 1회보다 짧게 두고 실행 시 목표 범위를 확인하는 방식이 장애 복구에 유리하다. 예를 들어 매일 실행하면서 현재 월부터 두 달 뒤까지 준비하는 정책을 둘 수 있다. 이는 설명용 정책이며 적정 선행 기간은 장애 대응 시간과 업무 입력 범위로 정한다. 한 번 실행에 만들 수 있는 월 수와 총 DDL 시간을 제한하여, 잘못된 기준 날짜가 수년치 작업으로 확산되지 않게 한다.

실행기는 다음 순서로 동작한다.

  1. 기준 시각·업무 시간대·유지할 첫 날짜·미래 목표 상한을 한 번 계산하고 실행 기록에 고정한다.
  2. writer에 연결하여 대상 스키마, 테이블 정의, 파티션 방식, 최종 유한 상한, p_future를 확인한다. 허용되지 않은 스키마 차이는 자동 수정하지 않고 중단한다.
  3. 같은 관리 잠금을 획득하고, 부족한 월을 한 달씩 생성한다. DDL 직전과 직후의 경계를 기록한다. 예제 프로시저를 호출할 때는 각 호출의 잠금 규칙과 외부 실행기의 직렬화 범위를 일관되게 구성한다.
  4. 오류나 연결 단절 시 먼저 스키마를 다시 읽는다. 클라이언트가 응답을 받지 못했어도 서버에서 DDL이 끝났을 수 있다. 테이블 상태를 확인하지 않고 같은 ALTER를 반복하지 않는다.
  5. 생성 완료를 확인한 후에만 삭제 후보를 계산한다. 별도 승인 및 보존 보류 목록을 적용하고, 승인 당시 경계와 실행 직전 경계가 같을 때 한 건씩 삭제한다.
  6. 삭제 후 파티션 부재와 남은 데이터 범위를 검증한다. 실행 기록에는 대상·상한·정책 버전·승인 식별자·DDL·소요 시간·결과를 남긴다. 이미 없는 파티션을 성공으로 처리하려면 해당 작업의 이전 성공 기록과 현재 경계도 대조한다.

프로시저 안의 lock_wait_timeout 설정이 아니라 호출 세션에서 설정한 값을 사용했다. MDL 대기 한도를 짧게 두는 것은 긴 대기열 확산을 줄이지만, ALTER의 전체 실행 시간을 제한하지는 않는다. 실행 시간 제한과 중단 후 재조회는 실행기의 별도 책임이다. 예제는 오류 시 관리 잠금을 해제하지만, 동적 SQL 실행 중 실패한 세션은 종료하여 준비된 문장 등 세션 자원도 정리하는 방식이 단순하다.

생성과 삭제를 하나의 트랜잭션으로 묶고 마지막에 ROLLBACK하는 설계는 성립하지 않는다. DDL은 암묵적 커밋 경계를 만들며, 여러 DDL로 구성된 유지보수 전체가 원자적으로 되돌아가지 않는다. 삭제 취소는 파티션 이름을 재생성하는 일이 아니라, 삭제된 데이터를 백업 또는 아카이브에서 복구하는 일이다.

8. MySQL 복제와 Aurora에서의 운영 차이

Community MySQL의 비동기 복제에서는 source의 DDL 성공 뒤에도 replica가 DDL을 적용하면서 읽기 지연이나 적용 지연이 생길 수 있다. 각 replica에서 생성 작업을 따로 돌리는 대신 source에서 수행한 변경의 적용 상태를 추적한다. 오래된 replica 읽기가 삭제 전 데이터를 잠시 보여 주는 경우와, 장기 읽기가 DDL 적용을 지연시키는 경우도 구별한다.

Aurora MySQL에서는 writer에서 DDL을 실행하고, 장애 조치 후에는 writer endpoint의 연결을 새로 맺은 다음 실제 파티션 상태를 다시 조회한다. 서버별 GET_LOCK()만으로 장애 조치 전후의 작업을 전역 직렬화했다고 간주하지 않는다. Aurora의 공유 스토리지 특성이 파티션 DDL의 쓰기 차단 가능성이나 입력 경계 설계를 없애 주지는 않는다. 정확한 버전의 DDL 지원, reader 응답 지연, writer 부하를 사전 환경에서 확인해야 한다.

이 글의 검증은 로컬 MySQL 8.0의 파티션 분할·재실행·요청 거부·삭제·지연 입력에 한정한다. Aurora 실행, 복제 지연, 실제 부하 중 잠금 시간, 스토리지 공간 회수 및 요금 변화는 측정하지 않았다. 파티션을 삭제했다고 즉시 스토리지 사용량이나 백업 보관량이 같은 크기로 줄어든다고 가정하지 않는다.

9. 운영 점검표와 실습 정리

  • 미리 준비할 기간과 p_future

실습을 마친 뒤 다음과 같이 실습 객체만 정리한다.

DROP PROCEDURE ensure_event_month;
DROP TABLE monthly_events;

실행 결과(MySQL 8.0.x):

mysql> DROP PROCEDURE ensure_event_month;

Query OK, 0 rows affected (0.01 sec)

mysql> DROP TABLE monthly_events;

Query OK, 0 rows affected (0.00 sec)

월별 파티션 자동화의 핵심은 날짜 문자열을 붙여 ALTER를 만드는 기술이 아니다. 실제 경계와 목표 경계를 비교하고, 부분 성공을 인식하며, 데이터 삭제를 독립적인 승인 작업으로 다루는 것이 핵심이다. 이 원칙은 이후 아카이빙, 대량 데이터 수명 관리, 파티션 교환을 이용한 데이터 이동에도 그대로 이어진다.

참고 문서