배치 처리 방법
INSERT INTO posts(...) VALUES
(1,2,3),
(1,2,3);-- Prepared Statement로 반복 INSERT 최적화
PREPARE stmt_insert_comment FROM
'INSERT INTO comments (post_id, user_id, content) VALUES (?, ?, ?)';
-- 변수 설정 후 실행 (루프에서 반복)
SET @post_id = 1;
SET @user_id = 1;
SET @content = '첫 번째 댓글';
EXECUTE stmt_insert_comment USING @post_id, @user_id, @content;
-- 다른 데이터로 재실행
SET @post_id = 1;
SET @user_id = 2;
SET @content = '두 번째 댓글';
SET @status = 'APPROVED';
EXECUTE stmt_insert_comment USING @post_id, @user_id, @content;
-- 정리
DEALLOCATE PREPARE stmt_insert_comment;
-- INSERT INTO
use crud_patterns;
-- 단일 사용자 등록
INSERT INTO users (username, email, password_hash, display_name)
VALUES ('john_doe', 'john@example.com', SHA2('password123', 256), 'John Doe');
-- 게시글 작성
INSERT INTO posts (user_id, category_id, title, content, status)
VALUES (1, 1, '제목', '내용', 'PUBLISHED');
-- 댓글 작성
INSERT INTO comments (post_id, user_id, content)
VALUES (1, 1, '댓글 내용');
INSERT INTO posts (user_id, category_id, title, content, status) VALUES
(1, 1, '제목1', '내용1', 'PUBLISHED'),
(1, 2, '제목2', '내용2', 'PUBLISHED'),
(2, 1, '제목3', '내용3', 'DRAFT');
-- Prepared Statement로 반복 INSERT 최적화
PREPARE stmt_insert_comment FROM
'INSERT INTO comments (post_id, user_id, content) VALUES (?, ?, ?)';
-- 변수 설정 후 실행 (루프에서 반복)
SET @post_id = 1;
SET @user_id = 1;
SET @content = '첫 번째 댓글';
EXECUTE stmt_insert_comment USING @post_id, @user_id, @content;
-- 다른 데이터로 재실행
SET @post_id = 1;
SET @user_id = 2;
SET @content = '두 번째 댓글';
SET @status = 'APPROVED';
EXECUTE stmt_insert_comment USING @post_id, @user_id, @content;
-- 정리
DEALLOCATE PREPARE stmt_insert_comment;
-- CSV 파일에서 직접 로드 (가장 빠른 방법)
LOAD DATA INFILE '/tmp/users_data.csv'
INTO TABLE users
FIELDS TERMINATED BY ','
LINES TERMINATED BY '\n'
IGNORE 1 ROWS -- 헤더 스킵
(username, email, display_name, @password)
SET password_hash = SHA2(@password, 256),
status = 'ACTIVE',
created_at = NOW();
-- 중복 시 에러 없이 건너뛰기
INSERT IGNORE INTO users (username, email, password_hash, display_name)
VALUES
('john_doe', 'john@example.com', SHA2('pass1', 256), 'John'),
('jane_doe', 'jane@example.com', SHA2('pass2', 256), 'Jane'),
('john_doe', 'john2@example.com', SHA2('pass3', 256), 'John2');
-- -> SELECT -> UPDATE or INSERT
-- 중복 시 특정 컬럼만 업데이트
INSERT INTO users (username, email, password_hash, display_name, login_count)
VALUES ('john_doe', 'john@example.com', SHA2('pass', 256), 'John Doe', 1)
ON DUPLICATE KEY UPDATE
display_name = VALUES(display_name),
login_count = login_count + 1, -- 기존값 + 1
updated_at = CURRENT_TIMESTAMP;
-- 복잡한 업데이트 로직
INSERT INTO posts (user_id, category_id, title, content, view_count)
VALUES (1, 1, '제목', '내용', 1)
ON DUPLICATE KEY UPDATE
content = CONCAT(content, '\n--- 업데이트됨 ---\n', VALUES(content)),
view_count = view_count + VALUES(view_count),
updated_at = CURRENT_TIMESTAMP;
-- 중복 시 기존 데이터 삭제 후 새 데이터 삽입
REPLACE INTO users (user_id, username, email, password_hash, display_name)
VALUES (1, 'john_updated', 'john_new@example.com', SHA2('newpass', 256), 'John Updated');
-- EXISTS를 이용한 조건부 삽입
INSERT INTO comments (post_id, user_id, content)
SELECT 1, 2, '댓글 내용'
WHERE EXISTS (
SELECT 1 FROM posts
WHERE post_id = 1 AND status = 'PUBLISHED'
)
AND NOT EXISTS (
SELECT 1 FROM comments
WHERE post_id = 1 AND user_id = 2
AND created_at > DATE_SUB(NOW(), INTERVAL 1 MINUTE)
);
-- 다른 테이블에서 데이터 복사
INSERT INTO posts (user_id, category_id, title, content, status)
SELECT
user_id,
1 as category_id,
CONCAT('임시 제목 - ', user_id) as title,
'자동 생성된 내용' as content,
'DRAFT' as status
FROM users
WHERE status = 'ACTIVE'
AND login_count > 10;
-- @Transactional
-- 다중 테이블 INSERT
START TRANSACTION;
-- 1. 게시글 작성
INSERT INTO posts (user_id, category_id, title, content, status)
VALUES (1, 1, '새 게시글', '내용', 'PUBLISHED');
SET @new_post_id = LAST_INSERT_ID();
-- 2. 첫 댓글 자동 생성
INSERT INTO comments (post_id, user_id, content, status)
VALUES (@new_post_id, 1, '게시글을 작성했습니다.', 'APPROVED');
COMMIT;
13. 복잡한 서비스 위한 SELECT 고급화 기법
커서 기반 페이징
SELECT
post_id,
title,
view_count,
created_at
FROM posts
WHERE status = 'PUBLISHED'
ORDER BY created_at DESC
LIMIT 10 OFFSET 0;
SELECT
post_id,
title,
view_count,
created_at
FROM posts
WHERE status = 'PUBLISHED'
ORDER BY created_at DESC
LIMIT 10 OFFSET 10;
SELECT post_id, title, created_at
FROM posts
WHERE status = 'PUBLISHED'
ORDER BY created_at DESC
LIMIT 10 OFFSET 10000;
-- 커서 기반의 페이징 첫번쨰 페이지
SELECT post_id, title, created_at
FROM posts
WHERE status = 'PUBLISHED'
ORDER BY created_at DESC, post_id DESC
LIMIT 10;
-- 커서 기반의 페이징 두번쨰 페이지
SELECT post_id, title, created_at
FROM posts
WHERE status = 'PUBLISHED' AND post_id < 12345
ORDER BY created_at DESC, post_id DESC
LIMIT 10;
SELECT
기본컬럼들,
윈도우함수() OVER (PARTITION BY 그룹컬럼 ORDER BY 정렬컬럼) as 결과컬럼
FROM 테이블;
SELECT
post_id,
title,
view_count,
-- 순차적인 순위 (1,2,3,4...)
ROW_NUMBER() OVER (ORDER BY view_count DESC) as 전체순위,
-- 동점자 고려한 순위 (1,2,2,4...)
RANK() OVER (ORDER BY view_count DESC) as 랭킹
FROM posts
WHERE status = 'PUBLISHED'
ORDER BY view_count DESC;
SELECT
user_id,
post_id,
title,
view_count,
-- 각 사용자 안에서의 순위
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY view_count DESC) as 사용자내순위
FROM posts
WHERE status = 'PUBLISHED'
ORDER BY user_id, 사용자내순위;
SELECT
post_id,
title,
created_at,
view_count,
-- 이전 게시글의 조회수
LAG(view_count, 1) OVER (ORDER BY created_at) as 이전글조회수,
-- 다음 게시글의 조회수
LEAD(view_count, 1) OVER (ORDER BY created_at) as 다음글조회수
FROM posts
WHERE status = 'PUBLISHED'
ORDER BY created_at;
WITH 임시테이블명 AS (
SELECT ... -- 1단계 계산
)
SELECT ... -- 2단계에서 1단계 결과 사용
FROM 임시테이블명;
WITH post_summary AS (
-- 1단계: 사용자별 게시글 통계 계산
SELECT
user_id,
COUNT(*) as 게시글수,
SUM(view_count) as 총조회수,
AVG(view_count) as 평균조회수
FROM posts
WHERE status = 'PUBLISHED'
GROUP BY user_id
)
-- 2단계: 사용자 정보와 결합해서 최종 결과
SELECT
u.username,
ps.게시글수,
ps.총조회수,
ps.평균조회수
FROM post_summary ps
INNER JOIN users u ON ps.user_id = u.user_id
ORDER BY ps.총조회수 DESC;
SELECT
u.username,
COUNT(*) as 게시글수,
SUM(p.view_count) as 총조회수,
AVG(p.view_count) as 평균조회수
FROM posts p
INNER JOIN users u ON p.user_id = u.user_id
WHERE p.status = 'PUBLISHED'
GROUP BY u.user_id, u.username
ORDER BY SUM(p.view_count) DESC;
WITH 활성사용자 AS (
-- 1단계: 활성 사용자만 추출
SELECT user_id, username
FROM users
WHERE status = 'ACTIVE'
),
최근게시글 AS (
-- 2단계: 최근 30일 게시글 통계
SELECT user_id, COUNT(*) as 최근글수
FROM posts
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
AND status = 'PUBLISHED'
GROUP BY user_id
)
-- 3단계: 두 결과를 조합
SELECT
a.username,
COALESCE(r.최근글수, 0) as 최근30일글수
FROM 활성사용자 a
LEFT JOIN 최근게시글 r ON a.user_id = r.user_id
ORDER BY 최근30일글수 DESC;
SELECT
u.username,
COALESCE(r.최근글수, 0) as 최근30일글수
FROM users u
LEFT JOIN (
SELECT user_id, COUNT(*) as 최근글수
FROM posts
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
AND status = 'PUBLISHED'
GROUP BY user_id
) r ON u.user_id = r.user_id
WHERE u.status = 'ACTIVE'
ORDER BY 최근30일글수 DESC;
SELECT
user_id,
username
FROM users u
WHERE status = 'ACTIVE'
AND EXISTS (
SELECT 1 -- 실제 값은 중요하지 않음, 존재만 확인
FROM posts p
WHERE p.user_id = u.user_id
AND p.created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
);
SELECT username
FROM users u
WHERE EXISTS (
SELECT 1 FROM posts p WHERE p.user_id = u.user_id
);
SELECT username
FROM users u
WHERE u.user_id IN (
SELECT user_id FROM posts
);
14. UPDATE, DELETE 완벽 가이드
UPDATE users
SET last_login_at = CURRENT_TIMESTAMP
WHERE user_id = 1;
UPDATE users
SET
display_name = '새로운 표시명',
profile_image_url = 'https://example.com/new-image.jpg',
updated_at = CURRENT_TIMESTAMP
WHERE user_id = 123 AND status = 'ACTIVE';
UPDATE posts
SET like_count = CASE
WHEN like_count > 0 THEN like_count - 1
ELSE like_count + 1
END
WHERE post_id = 1;
UPDATE posts
SET status = CASE
WHEN view_count < 10 THEN 'DRAFT' -- 조회수 낮으면 임시저장
WHEN view_count >= 1000 THEN 'FEATURED' -- 조회수 높으면 추천글
ELSE 'PUBLISHED' -- 나머지는 일반 발행
END,
updated_at = CURRENT_TIMESTAMP
WHERE created_at < DATE_SUB(NOW(), INTERVAL 30 DAY);
UPDATE posts p
INNER JOIN users u ON p.user_id = u.user_id
SET p.author_name = u.display_name
WHERE u.updated_at > p.updated_at;
UPDATE users
SET status = 'INACTIVE'
WHERE last_login_at < DATE_SUB(NOW(), INTERVAL 90 DAY)
AND status = 'ACTIVE'
LIMIT 1000;
DELIMITER //
CREATE PROCEDURE BatchUpdateInactiveUsers()
BEGIN
DECLARE affected_rows INT DEFAULT 1;
WHILE affected_rows > 0 DO
UPDATE users
SET status = 'INACTIVE',
updated_at = CURRENT_TIMESTAMP
WHERE last_login_at < DATE_SUB(NOW(), INTERVAL 90 DAY)
AND status = 'ACTIVE'
LIMIT 1000;
SET affected_rows = ROW_COUNT(); -- 실제 수정된 행 수
SELECT SLEEP(0.1); -- 0.1초 대기 (시스템 부하 방지)
END WHILE;
END //
DELIMITER ;
CALL BatchUpdateInactiveUsers();
DELIMITER $$
CREATE PROCEDURE safe_deduct_points()
BEGIN
DECLARE current_points INT DEFAULT 0;
-- 트랜잭션 시작
START TRANSACTION;
-- 1. 현재 포인트 확인 (락 걸기)
SELECT COALESCE(points, 0) INTO current_points
FROM users
WHERE user_id = 1 FOR UPDATE;
-- 2. 포인트가 충분한지 검증
IF current_points >= 100 THEN
-- 3. 포인트 차감
UPDATE users
SET points = points - 100,
updated_at = CURRENT_TIMESTAMP
WHERE user_id = 1;
-- 4. 사용 내역 기록
INSERT INTO point_history (user_id, amount, type, description)
VALUES (1, -100, 'USE', '상품 구매');
COMMIT; -- 성공
ELSE
ROLLBACK; -- 포인트 부족으로 실패
END IF;
END$$
DELIMITER ;
-- ---------------------------------------------------------------------------------------------------
DELETE FROM comments
WHERE comment_id = 456
AND user_id = 123;
UPDATE posts
SET status = 'DELETED',
updated_at = CURRENT_TIMESTAMP
WHERE post_id = 1 AND user_id = 1;
DELIMITER //
CREATE PROCEDURE BatchDeleteOldData()
BEGIN
DECLARE affected_rows INT DEFAULT 1;
WHILE affected_rows > 0 DO
-- 30일 이상 삭제된 댓글 실제 삭제
DELETE FROM comments
WHERE status = 'DELETED'
AND updated_at < DATE_SUB(NOW(), INTERVAL 30 DAY)
LIMIT 1000;
SET affected_rows = ROW_COUNT();
-- 진행상황 출력
SELECT CONCAT('Deleted ', affected_rows, ' comments') as progress;
SELECT SLEEP(0.5); -- 0.5초 대기
END WHILE;
END //
DELIMITER ;
CALL BatchDeleteOldData();
ALTER TABLE logs DROP PARTITION p_202301;