기록

MySQL Unions, Events, Triggers

als982001 2025. 4. 6. 17:57

 

1. 작성한 코드

-- movies normalization

-- Normalization Status
CREATE TABLE statuses (
  status_id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  status_name ENUM (
    'Canceled',
    'In Production',
    'Planned',
    'Post Production',
    'Released',
    'Rumored'
	) NOT NULL,
  explanation TEXT,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL
);

INSERT 
	INTO statuses (status_name) 
SELECT status FROM movies GROUP BY status;
 
ALTER TABLE movies ADD COLUMN status_id BIGINT UNSIGNED; 
  
ALTER TABLE movies ADD CONSTRAINT fk_status FOREIGN KEY (status_id) REFERENCES statuses (status_id) ON DELETE SET NULL;   
  
UPDATE movies SET status_id = (SELECT status_id FROM statuses WHERE status_name = movies.status);  
  
ALTER TABLE movies DROP COLUMN status; 
  
  
-- Normalizing Directors
CREATE TABLE directors (
  director_id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(120),
	created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL
);

INSERT INTO 
	directors (name)
SELECT 
	director 
FROM movies 
GROUP BY 
	director 
HAVING director <> '';
  
ALTER TABLE movies ADD COLUMN director_id BIGINT UNSIGNED;

ALTER TABLE movies ADD CONSTRAINT fk_director FOREIGN KEY (director_id) REFERENCES directors (director_id) ON DELETE SET NULL;
 
CREATE INDEX idx_director_name ON directors (name);
 
UPDATE 
	movies 
SET 
	director_id = (
    SELECT director_id 
    FROM directors 
    WHERE name = movies.director
);
  
ALTER TABLE movies DROP COLUMN director;
  
-- Normalizing Original Language
CREATE TABLE langs (
	lang_id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(120),
  code CHAR(2),
	created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL
);

INSERT INTO langs (code)
SELECT original_language
FROM movies
GROUP BY original_language;

ALTER TABLE movies ADD COLUMN original_lang_id BIGINT UNSIGNED;

ALTER TABLE movies
ADD CONSTRAINT fk_org_lang FOREIGN KEY (original_lang_id) 
REFERENCES langs (lang_id) ON DELETE SET NULL;

UPDATE movies SET original_lang_id = (SELECT lang_id FROM langs WHERE code = movies.original_language);

ALTER TABLe movies DROP COLUMN original_language;

-- Normalizing Countries
CREATE TABLE countries (
  country_id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  country_code CHAR(2) UNIQUE NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL
);

INSERT INTO countries (country_code)
SELECT country 
FROM movies 
WHERE country NOT LIKE '%,%'
GROUP BY country;

-- JOIN은 기본적으로 테이블을 수평적으로 합침(수평적으로 테이블을 확장)
-- UNION은 기본적으로 테이블을 합쳐주는데, 중복된 값들은 삭제함
-- UNION ALL은 중복된 값을 삭제 안함
SELECT 'a', 1
UNION
SELECT 'b', 2;

INSERT IGNORE INTO countries (country_code)
SELECT 
  SUBSTRING_INDEX(country, ',', 1)
FROM movies 
WHERE country LIKE '__,__'
GROUP BY country
UNION
SELECT 
  SUBSTRING_INDEX(country, ',', -1)
FROM movies 
WHERE country LIKE '__,__'
GROUP BY country;

INSERT IGNORE INTO countries (country_code)
SELECT 
  SUBSTRING_INDEX(country, ',', 1)
FROM movies 
WHERE country LIKE '__,__,__'
GROUP BY country
UNION
SELECT 
  SUBSTRING_INDEX(country, ',', -1)
FROM movies 
WHERE country LIKE '__,__,__'
GROUP BY country
UNION
SELECT 
  SUBSTRING_INDEX(SUBSTRING_INDEX(country, ',', 2), ',', -1)
FROM movies 
WHERE country LIKE '__,__,__'
GROUP BY country;

SELECT SUBSTRING_INDEX('gb,us,it', ',', 1);

CREATE TABLE movies_countries (
  movie_id BIGINT UNSIGNED,
  country_id BIGINT UNSIGNED,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL,
  PRIMARY KEY (movie_id, country_id),
  FOREIGN KEY (movie_id) REFERENCES movies (movie_id) ON DELETE CASCADE,
  FOREIGN KEY (country_id) REFERENCES countries (country_id) ON DELETE CASCADE
);

INSERT INTO movies_countries (movie_id, country_id)
SELECT
  movies.movie_id, 
  countries.country_id
FROM movies
	JOIN countries ON movies.country LIKE CONCAT('%', countries.country_code, '%')
	WHERE movies.country <> '';

ALTER TABLE movies DROP COLUMN country;
-- table 구조 복사
CREATE TABLE archived_movies LIKE movies;

DROP TABLE archived_movies;

-- STARTS: 얼마나 시간이 지나고 시작할지 지정. 없으면 바로 시작함
DELIMITER $$
CREATE EVENT archive_old_movies 
ON SCHEDULE EVERY 2 MINUTE
STARTS CURRENT_TIMESTAMP + INTERVAL 2 MINUTE
-- DO는 오직 statement가 1개일 때만 작동
-- INSERT, DELETE 2개의 분리된 statement를 가지고 있으므로 BEGIN + END 입력
-- delimiter 변경도 필요
DO
BEGIN
	INSERT INTO archived_movies
  SELECT * FROM movies 
  WHERE release_date < YEAR(CURDATE()) - 20;
	DELETE FROM movies release_date < YEAR(CURDATE()) - 20; 
END$$
DELIMITER ;

CREATE EVENT archive_old_movies 
ON SCHEDULE 
  EVERY 2 MINUTE 
  STARTS CURRENT_TIMESTAMP + INTERVAL 2 MINUTE
DO
BEGIN
  INSERT INTO archived_movies
  SELECT * FROM movies 
  WHERE release_date < YEAR(CURDATE()) - 20;

  DELETE FROM movies 
  WHERE release_date < YEAR(CURDATE()) - 20;
END;

DROP EVENT archive_old_movies;

SHOW events;

-- BEFORE: INSERT, UPDATE, DELETE
-- AFTER: INSERT, UPDATE, DELETE

CREATE TABLE records (
  record_id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  changes TINYTEXT,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);

CREATE TRIGGER before_movie_insert
BEFORE INSERT
ON movies
FOR EACH ROW
INSERT INTO records (changes) VALUES (CONCAT('Will Insert ', NEW.title));

CREATE TRIGGER after_movie_insert
AFTER INSERT
ON movies
FOR EACH ROW
INSERT INTO records (changes) VALUES (CONCAT('Insert completed: ', NEW.title));

CREATE TRIGGER before_movie_update
BEFORE UPDATE
ON movies
FOR EACH ROW
INSERT INTO records (changes) VALUES (CONCAT('Will update title: ', OLD.title, ' -> ', NEW.title));

CREATE TRIGGER after_movie_update
AFTER UPDATE
ON movies
FOR EACH ROW
INSERT INTO records (changes) VALUES (CONCAT('Will update title: ', OLD.title, ' -> ', NEW.title));

CREATE TRIGGER before_movie_delete
BEFORE DELETE
ON movies
FOR EACH ROW
INSERT INTO records (changes) VALUES (CONCAT('Will deleted: ', OLD.title));

CREATE TRIGGER after_movie_delete
AFTER DELETE
ON movies
FOR EACH ROW
INSERT INTO records (changes) VALUES (CONCAT('bye bye ', OLD.title));


INSERT INTO movies SELECT * FROM archived_movies WHERE movie_id = 2;

SHOW TRIGGERS;

DROP TRIGGER after_movie_update;

TRUNCATE TABLE records;

CREATE TRIGGER after_movie_update
AFTER UPDATE
ON movies
FOR EACH ROW
BEGIN
	DECLARE changes TINYTEXT DEFAULT '';
  
  IF NEW.title <> OLD.title THEN 
  	SET changes = CONCAT('Title changed ', OLD.title, ' -> ', NEW.title, '\n');
  END IF;
  
  IF NEW.budget <> OLD.budget THEN
  	SET changes = CONCAT(changes, 'Budget changed ', OLD.budget, ' -> ', NEW.budget);
  END IF;
  
  INSERT INTO records (changes) VALUES (changes);
END;

DROP TRIGGER after_movie_update;

 

2. 배운 개념

2-1. 정규화 (Normalization)

  • 중복을 제거하고 데이터의 무결성을 유지하기 위해 테이블을 쪼개는 작업
  • 정규화의 목적
    • 중복 제거
    • 데이터 일관성 유지
    • 변경 시 관리 용이성 확보

 

2-1-1. 정규화 예제

2-1-1-1. 상태(status) 정규화

-- 1. 상태 테이블 생성
CREATE TABLE statuses (...);

-- 2. 고유 상태 INSERT
INSERT INTO statuses (status_name) 
SELECT status FROM movies GROUP BY status;

-- 3. 외래키 컬럼 추가 + 매핑
ALTER TABLE movies ADD COLUMN status_id ...
UPDATE movies SET status_id = ...
ALTER TABLE movies DROP COLUMN status;
  • movies.status -> statuses.status_id로 정규화
  • 중복 상태명 제거, 참조 무결성 확보

2-1-1-2. 감독(director) 정규화

CREATE TABLE directors (...);
INSERT INTO directors (name) SELECT director FROM movies GROUP BY director HAVING director <> '';
ALTER TABLE movies ADD COLUMN director_id ...
UPDATE movies SET director_id = ...
ALTER TABLE movies DROP COLUMN director;
  • movies.director -> directors.director_id로 분리
  • 문자열 직접 저장 대신 ID 참조로 일관성 유지

2-1-1-3. 국가(country) 정규화 + 다대다 관계 구성

CREATE TABLE countries (...);

-- 단일 국가 추출
INSERT INTO countries (country_code)
SELECT country FROM movies WHERE country NOT LIKE '%,%' GROUP BY country;

-- 다중 국가 처리 (UNION + SUBSTRING_INDEX로 쪼갬)
-- "us,fr" → "us" + "fr", "gb,us,it" → 각각 쪼개서 UNION

-- 중복 방지 위해 INSERT IGNORE
-- 최종적으로 다대다 관계 테이블 생성
CREATE TABLE movies_countries (...);
INSERT INTO movies_countries (...)
SELECT movies.movie_id, countries.country_id
FROM movies JOIN countries ON movies.country LIKE CONCAT('%', countries.country_code, '%');
  • movies.country는 "US,FR"처럼 여러 국가를 한 컬럼에 포함하고 있음 
    • M:N 관계이기에 별도의 bridge table이 필요
  • movies.country -> movies_countries (movie_id, country_id)로 정규화
  • 쿼리 최적화 및 다국적 영화 처리 용이

 

2-2. UNION / UNION ALL

구분 JOIN UNION
방향 수평 (컬럼 확장) 수직 (행 확장)
목적 테이블 연결 결과 집합 합치기
조건 ON, USING 등 연결 조건 필요 컬럼 개수 & 타입 일치 필요
중복 제거 해당 없음  UNION: 중복 제거 / UNION ALL: 중복 유지

 

2-2-1. UNION, SUBSTRING_INDEX 예시

SELECT 'a', 1
UNION
SELECT 'b', 2;
  • 결과: a,1 / b,2 (중복 없으면 그대로, 중복 있으면 제거)
-- "us,fr" → us + fr 나누기 위해 SUBSTRING_INDEX 사용
SELECT SUBSTRING_INDEX(country, ',', 1)
UNION
SELECT SUBSTRING_INDEX(country, ',', -1)
  • UNION을 통해 국가 코드들을 중복 없이 INSERT

 

2-3.EVENT

  • 특정 시점에 자동으로 실행되는 작업으로, 미리 정해둔 시간이나 주기마다 실행되는 자동 작업 스케쥴러(쿼리)

2-3-1. 문법 구조

CREATE EVENT event_name
ON SCHEDULE
  EVERY [간격]
  STARTS [시작 시간]
DO
BEGIN
  -- 실행할 여러 쿼리
END;

 

2-3-2. 오래된 영화 아카이브

CREATE EVENT archive_old_movies 
ON SCHEDULE 
  EVERY 2 MINUTE 
  STARTS CURRENT_TIMESTAMP + INTERVAL 2 MINUTE
DO
BEGIN
  INSERT INTO archived_movies
  SELECT * FROM movies 
  WHERE release_date < YEAR(CURDATE()) - 20;

  DELETE FROM movies 
  WHERE release_date < YEAR(CURDATE()) - 20;
END;
  • 20년 이상된 영화들을 자동 백업 + 삭제하는 이벤트
  • STARTS를 설정하지 않으면 즉시 시작
  • 여러 쿼리를 쓸 때는 반드시 BEGIN ... END 사용

 

2-3-3. EVENT 관리 명령어

명령어 설명
SHOW EVENTS; 등록된 이벤트 목록 보기
DROP EVENT event_name; 이벤트 삭제
ALTER EVENT ... ENABLE/DISABLE; 이벤트 켜기/끄기
SET GLOBAL event_schedular = ON; 이벤트 스케쥴러 활성화 (기본은 OFF)

 

2-4. TRIGGER

  • 테이블에 INSERT, UPDATE, DELETE 같은 변화가 생길 때 자동으로 실행되는 로직

 

2-4-1. 문법 구조

CREATE TRIGGER trigger_name
[BEFORE | AFTER] [INSERT | UPDATE | DELETE]
ON table_name
FOR EACH ROW
[BEGIN]
  -- 트리거 실행 내용
[END]

 

2-4-2. BEFORE/AFTER INSERT

CREATE TRIGGER before_movie_insert
BEFORE INSERT ON movies
FOR EACH ROW
INSERT INTO records (changes) VALUES (CONCAT('Will Insert ', NEW.title));

CREATE TRIGGER after_movie_insert
AFTER INSERT ON movies
FOR EACH ROW
INSERT INTO records (changes) VALUES (CONCAT('Insert completed: ', NEW.title));

 

2-4-3. AFTER UPDATE

CREATE TRIGGER after_movie_update
AFTER UPDATE ON movies
FOR EACH ROW
BEGIN
  DECLARE changes TINYTEXT DEFAULT '';
  
  IF NEW.title <> OLD.title THEN 
    SET changes = CONCAT('Title changed ', OLD.title, ' -> ', NEW.title, '\n');
  END IF;
  
  IF NEW.budget <> OLD.budget THEN
    SET changes = CONCAT(changes, 'Budget changed ', OLD.budget, ' -> ', NEW.budget);
  END IF;
  
  INSERT INTO records (changes) VALUES (changes);
END;
  • NEW, OLD는 각각 변경 후, 변경 전의 값
  • 여러 컬럼도 비교 가능
  • BEGIN ... END로 여러 구문 처리 가능

 

2-4-4. BEFORE/AFTER DELETE

CREATE TRIGGER before_movie_delete
BEFORE DELETE ON movies
FOR EACH ROW
INSERT INTO records (changes) VALUES (CONCAT('Will deleted: ', OLD.title));

CREATE TRIGGER after_movie_delete
AFTER DELETE ON movies
FOR EACH ROW
INSERT INTO records (changes) VALUES (CONCAT('bye bye ', OLD.title));

 

2-4-5. TRIGGER 관리 명령어

명령어 설명
SHOW TRIGGERS; 트리거 목록 확인
DROP TRIGGER trigger_name; 트리거 삭제
TRUNCATE TABLE records; 트리거 로그 비우기
INSERT INTO ...  트리거 자동 실행되는 대상 조작

 

2-5. NEW/OLD 사용 규칙

트리거 타입 사용 가능한 키워드
BEFORE/AFTER INSERT NEW만 가능
BEFORE/AFTER UPDATE OLD, NEW 둘 다 가능
BEFORE/AFTER DELETE OLD만 가능

 

2-6. EVENT, TRIGGER 정리 요약

구분 EVENT TRIGGER
자동 실행 조건 설정된 시간/주기 테이블 변화(삽입/수정/삭제) 발생 시
주요 용도 백업, 정리, 통계 로그, 유효성 검사, 자동 기록
실행 주체 MySQL 스케쥴러 테이블 변경 트리거
BEGIN/END 필요 여부 여러 쿼리 시 필요 여러 쿼리 시 필요
활용 예시 오래된 데이터 주기적 백업 변경 이력 자동 기록