본문 바로가기

IT

MySQL 테이블 생성 시점과 마지막 CRUD 시점 확인하는 방법

728x90
반응형
728x170

운영 중인 데이터베이스를 들여다보다 보면 누구나 한 번쯤 이런 생각을 하게 됩니다. "이 테이블 도대체 언제 만들어진 거지?", "이거 아직도 누가 쓰고 있는 건가, 아니면 죽은 테이블인가?" 특히 users_old, temp_backup_20231201 같이 의문스러운 이름의 테이블이 줄줄이 있을 때는 더더욱 그렇습니다. 지우자니 누가 쓸까 봐 무섭고, 두자니 디스크가 아까운 그 미묘한 감정. 이번 글에서는 MySQL에서 테이블 생성 시점과 마지막 CRUD 시점을 정확하게 파악하는 방법을 정리해 보겠습니다.

information_schema로 기본 정보 확인하기

MySQL이 친절하게도 테이블 메타데이터를 모아둔 시스템 데이터베이스가 있습니다. 바로 information_schema입니다. 가장 먼저 시도해볼 쿼리는 다음과 같습니다.

SELECT 
    TABLE_SCHEMA,
    TABLE_NAME,
    CREATE_TIME,
    UPDATE_TIME,
    CHECK_TIME,
    TABLE_ROWS,
    ENGINE
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'your_database'
  AND TABLE_NAME = 'your_table';

 

각 컬럼이 알려주는 정보는 다음과 같습니다.

컬럼명 의미
CREATE_TIME 테이블이 생성된 시점
UPDATE_TIME 데이터가 마지막으로 변경된 시점(INSERT/UPDATE/DELETE)
CHECK_TIME CHECK TABLE 명령이 마지막으로 실행된 시점
TABLE_ROWS 대략적인 행 개수(InnoDB는 정확하지 않음)

쿼리 한 번이면 끝나니까 정말 편리해 보입니다. 그런데 이 정보를 100퍼센트 믿고 의사결정을 하면 곤란한 상황이 생길 수 있습니다. 왜 그런지 하나씩 짚어보겠습니다.

CREATE_TIME과 UPDATE_TIME의 함정

겉으로 보기엔 깔끔하지만, 이 두 컬럼은 생각보다 변덕스럽습니다.

CREATE_TIME이 갑자기 바뀌는 이유

테이블을 만든 뒤로 한 번도 손대지 않았는데 CREATE_TIME이 어제로 찍혀 있을 수도 있습니다. 이유는 간단합니다. ALTER TABLE, OPTIMIZE TABLE, TRUNCATE TABLE 같은 작업을 수행하면 InnoDB 내부적으로 테이블이 재생성되면서 이 시각이 갱신됩니다. 컬럼 하나만 추가했을 뿐인데 생성 시점이 바뀌어 버리는 거죠.

 

즉, CREATE_TIME은 "테이블이 최초로 만들어진 시점"이 아니라 "현재 디스크상에 존재하는 테이블 파일이 만들어진 시점"에 더 가깝습니다.

UPDATE_TIME이 NULL로 나오는 이유

이게 더 골치 아픕니다. MySQL 5.7.2 이상부터 InnoDB 테이블의 UPDATE_TIME이 지원되긴 하지만, 다음과 같은 상황에서는 값이 사라집니다.

  • 서버가 재시작되면 메모리에 있던 정보가 날아갑니다
  • 변경사항이 아직 버퍼 풀에만 있고 디스크에 flush되지 않았다면 NULL로 표시될 수 있습니다
  • 일정 시간 이상 데이터 딕셔너리 캐시에서 해당 테이블이 빠지면 마찬가지로 NULL이 됩니다

운영 서버를 자주 재기동하지 않더라도 이 값은 영구적으로 보장되지 않습니다. 따라서 UPDATE_TIME을 "마지막 CRUD 시점의 절대적 증거"로 삼는 건 위험합니다.

SELECT 시점은 알 수 없다

CRUD라는 단어를 쓸 때 보통 Read까지 포함하는데, 안타깝게도 information_schema는 SELECT 시점을 추적하지 않습니다. 즉, "이 테이블에 마지막으로 조회 쿼리가 들어온 게 언제냐"는 질문에는 이 방법으로 답할 수 없습니다.

Performance Schema로 접근 패턴 살펴보기

Read까지 추적하고 싶다면 Performance Schema를 활용할 수 있습니다.

SELECT 
    OBJECT_SCHEMA,
    OBJECT_NAME,
    COUNT_READ,
    COUNT_WRITE,
    COUNT_FETCH,
    COUNT_INSERT,
    COUNT_UPDATE,
    COUNT_DELETE
FROM performance_schema.table_io_waits_summary_by_table
WHERE OBJECT_SCHEMA = 'your_database'
  AND OBJECT_NAME = 'your_table';

다만 이 통계도 만능은 아닙니다. 서버가 재시작되면 카운터가 0으로 초기화되고, "마지막 접근 시각" 자체를 직접 노출하는 표준 컬럼이 없습니다. 즉, "최근에 얼마나 자주 쓰였는가"는 알 수 있어도 "정확히 몇 시 몇 분에 마지막으로 쓰였는가"는 알기 어렵습니다.

그래도 카운트가 0에 가까운 테이블이라면 사실상 죽은 테이블이라고 판단할 수 있어서 유용한 지표가 됩니다.

가장 확실한 방법, Binary Log 추적

진짜로 "마지막 INSERT/UPDATE/DELETE가 언제 어떤 값으로 일어났는지"를 정확히 알고 싶다면 Binary Log가 정답입니다. 운영 환경에서는 복제나 백업 목적으로 binlog가 켜져 있는 경우가 대부분입니다.

먼저 binlog가 활성화돼 있는지 확인합니다.

SHOW VARIABLES LIKE 'log_bin';
SHOW BINARY LOGS;

ON으로 나온다면 다음과 같이 mysqlbinlog 도구로 특정 테이블에 대한 변경 이벤트를 조회할 수 있습니다.

mysqlbinlog --start-datetime="2026-05-01 00:00:00" \
            --database=your_database \
            /var/lib/mysql/binlog.000123 \
  | grep -i "your_table"

binlog 보존 기간(binlog_expire_logs_seconds)을 넘어선 이력은 당연히 추적할 수 없습니다. 운영 환경에서 변경 이력 추적이 중요한 시스템이라면 보존 기간을 충분히 길게 설정해 두는 것이 좋습니다.

컬럼 기반 추적이 가장 깔끔하다

사실 가장 골치 아프지 않은 방법은 처음부터 테이블에 created_at, updated_at 컬럼을 두는 것입니다. 다음과 같은 형태가 일반적입니다.

CREATE TABLE example (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    content TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

이렇게 두면 다음 쿼리 한 줄로 끝납니다.

SELECT 
    MIN(created_at) AS first_insert,
    MAX(created_at) AS last_insert,
    MAX(updated_at) AS last_modified
FROM your_database.your_table;

행 단위 추적이 가능하고, 서버 재시작이나 ALTER 같은 외부 요인의 영향을 받지 않습니다. Spring Boot의 JPA Auditing이나 MyBatis 인터셉터를 쓰면 코드 레벨에서도 자동으로 채워줄 수 있어서 거의 공짜에 가깝습니다.

본격적으로 추적하려면 감사 테이블

장기적으로 변경 이력을 모두 보관해야 한다면 별도의 audit 테이블을 두는 게 정석입니다. 트리거를 활용한 간단한 예시는 다음과 같습니다.

CREATE TABLE your_table_audit (
    audit_id BIGINT AUTO_INCREMENT PRIMARY KEY,
    operation VARCHAR(10),
    target_id BIGINT,
    changed_by VARCHAR(100),
    changed_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

DELIMITER $$
CREATE TRIGGER trg_your_table_after_update
AFTER UPDATE ON your_table
FOR EACH ROW
BEGIN
    INSERT INTO your_table_audit (operation, target_id, changed_by)
    VALUES ('UPDATE', NEW.id, CURRENT_USER());
END$$
DELIMITER ;

규모가 커지면 트리거 대신 CDC(Change Data Capture) 솔루션이나 Debezium 같은 도구를 도입하는 편이 운영상 더 깔끔합니다.

상황별 추천 전략

지금까지 살펴본 방법들을 상황에 맞춰 정리하면 다음과 같습니다.

상황 추천 방법
빠르게 대략적인 시점만 확인 information_schema.TABLES
죽은 테이블 여부 판단 Performance Schema 접근 카운트
정확한 마지막 변경 시점 필요 Binary Log 분석
앞으로의 추적을 자동화 created_at, updated_at 컬럼 추가
변경 이력 전체 보존 Audit 테이블 또는 CDC 도구

실무에서 자주 만나는 시나리오

테이블 이름 뒤에 _old, _new, _backup, _tmp 같은 접미사가 붙어 있을 때가 가장 흔합니다. 보통 이런 경우는 마이그레이션이나 핫픽스 작업 흔적인 경우가 많은데, CREATE_TIME을 비교해서 어느 쪽이 나중에 만들어졌는지 확인한 뒤, UPDATE_TIME 또는 Performance Schema 카운트로 실제 사용 여부를 판별하는 게 일반적인 순서입니다.

DESC로 컬럼 구조부터 확인하는 것도 잊지 마세요. updated_at 같은 컬럼이 이미 있다면 굳이 binlog까지 뒤질 필요가 없습니다.

DESC your_database.your_table;

여기서 시간 관련 컬럼이 보이면 그쪽이 훨씬 빠르고 정확합니다.

마무리하며

MySQL의 테이블 메타데이터는 생각보다 휘발성이 강합니다. information_schema만 믿고 "이 테이블 안 쓰는 것 같으니 지우자"라고 결정했다가 실제로는 일주일에 한 번씩 돌아가는 배치가 의존하고 있어서 다음 주 월요일에 사고가 터지는 경우가 종종 있습니다. 따라서 중요한 의사결정 전에는 information_schema, Performance Schema, Binary Log, 컬럼 기반 추적을 적절히 조합해서 교차 검증하는 습관을 들이는 것이 안전합니다.

 

가장 좋은 건 처음 테이블을 설계할 때부터 created_at, updated_at 같은 메타 컬럼을 표준으로 박아두는 것입니다. 미래의 자신과 미래의 동료가 두고두고 고마워할 겁니다.

참고 자료

728x90
반응형
그리드형