JSCODE Logo
프로그래밍 과외블로그후기멘토진
회사명 : JSCODE대표 : 박재성사업자 등록번호 : 244-22-01557통신판매업 : 제 2023-인천미추홀-0381 호
학원 명칭 : 제이에스코드(JSCODE)원격학원학원설립ㆍ운영 등록번호 : 제6063호

서울특별시 구로구 경인로 20가길 11(오류동, 아델리아)

Copyright ⓒ 2025 JSCODE - 최상위 현업 개발자들의 프로그래밍 교육 All rights reserved.

이용약관개인정보처리방침
← 블로그 목록으로 돌아가기

[실습] WHERE문이 사용된 SQL문 튜닝하기 - 2

JSCODE 박재성
JSCODE 박재성
2025-12-02
author
JSCODE 박재성
category
MySQL
createdAt
Dec 2, 2025
series
비전공자도 이해할 수 있는 MySQL 성능 최적화 입문/실전 (SQL 튜닝편)
slug
where-clause-tuning-2
type
post
updatedAt
Dec 2, 2025 10:54 AM

✅ Sales 부서이면서 최근 3일 이내에 가입한 유저 조회하기

  1. 테이블 생성
    1. DROP TABLE IF EXISTS users; CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100), department VARCHAR(100), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );
 
  1. 100만 건의 랜덤 데이터 삽입
    1. -- 높은 재귀(반복) 횟수를 허용하도록 설정 -- (아래에서 생성할 더미 데이터의 개수와 맞춰서 작성하면 된다.) SET SESSION cte_max_recursion_depth = 1000000; -- 더미 데이터 삽입 쿼리 INSERT INTO users (name, department, created_at) WITH RECURSIVE cte (n) AS ( SELECT 1 UNION ALL SELECT n + 1 FROM cte WHERE n < 1000000 -- 생성하고 싶은 더미 데이터의 개수 ) SELECT CONCAT('User', LPAD(n, 7, '0')) AS name, -- 'User' 다음에 7자리 숫자로 구성된 이름 생성 CASE WHEN n % 10 = 1 THEN 'Engineering' WHEN n % 10 = 2 THEN 'Marketing' WHEN n % 10 = 3 THEN 'Sales' WHEN n % 10 = 4 THEN 'Finance' WHEN n % 10 = 5 THEN 'HR' WHEN n % 10 = 6 THEN 'Operations' WHEN n % 10 = 7 THEN 'IT' WHEN n % 10 = 8 THEN 'Customer Service' WHEN n % 10 = 9 THEN 'Research and Development' ELSE 'Product Management' END AS department, -- 의미 있는 단어 조합으로 부서 이름 생성 TIMESTAMP(DATE_SUB(NOW(), INTERVAL FLOOR(RAND() * 3650) DAY) + INTERVAL FLOOR(RAND() * 86400) SECOND) AS created_at -- 최근 10년 내의 임의의 날짜와 시간 생성 FROM cte; -- 잘 생성됐는 지 확인 SELECT COUNT(*) FROM users; SELECT * FROM users LIMIT 10;
 
  1. 데이터 조회해서 성능 측정하기
    1. SELECT * FROM users WHERE department = 'Sales' AND created_at >= DATE_SUB(NOW(), INTERVAL 3 DAY)
      notion image
      약 200ms 정도의 시간이 걸린다.
 
  1. 실행 계획 조회해보기
    1. # 실행 계획 EXPLAIN SELECT * FROM users WHERE department = 'Sales' AND created_at >= DATE_SUB(NOW(), INTERVAL 3 DAY); # 실행 계획 세부 내용 EXPLAIN ANALYZE SELECT * FROM users WHERE department = 'Sales' AND created_at >= DATE_SUB(NOW(), INTERVAL 3 DAY);
      notion image
      type이 ALL인걸 보니 풀 테이블 스캔을 했다. 비효율적이다. rows가 996,810인 걸 보니 전체 데이터를 대부분 액세스 했다는 걸 추측할 수 있다.
       
      -> Filter: ((users.department = 'Sales') and (users.created_at >= <cache>((now() - interval 3 day)))) (cost=93877 rows=33224) (actual time=12.6..205 rows=114 loops=1) -> Table scan on users (cost=93877 rows=996810) (actual time=0.0994..152 rows=1e+6 loops=1)
      EXPLAIN ANLYZE는 아래 화살표부터 위로 읽어나가야 한다.
    2. 풀 테이블 스캔을 했다.
      1. → 이 때, 액세스한 데이터의 개수는 1e+6(= 10의 6제곱 = 1,000,000)개 이다.
    3. 액세스한 1,000,000개의 데이터 중 department = ‘Sales’와 created_at >= DATE_SUB(NOW(), INTERVAL 3 DAY)을 만족하는 데이터를 필터링해온다.
      1. → 조건을 만족한 데이터의 개수는 rows는 114개이다.
 
 
  1. 성능 개선을 위한 인덱스 추가
    1. 인덱스를 추가하는 방법이 3가지가 있다.
    2. created_at 컬럼을 기준으로 인덱스 생성
    3. department 컬럼을 기준으로 인덱스 생성
    4. department, created_at 둘 다 인덱스 생성
    5.  
      [created_at 컬럼을 기준으로 인덱스 생성]
      CREATE INDEX idx_created_at ON users (created_at); # 성능 측정 SELECT * FROM users WHERE department = 'Sales' AND created_at >= DATE_SUB(NOW(), INTERVAL 3 DAY); # 실행 계획 EXPLAIN SELECT * FROM users WHERE department = 'Sales' AND created_at >= DATE_SUB(NOW(), INTERVAL 3 DAY); # 실행 계획 세부 내용 EXPLAIN ANALYZE SELECT * FROM users WHERE department = 'Sales' AND created_at >= DATE_SUB(NOW(), INTERVAL 3 DAY);
      notion image
      기존 200ms 응답 속도에서 20ms로 줄어들었으니 약 10배 정도의 성능이 향상됐다.
       
      notion image
      type이 range로 나온걸로 봐서 인덱스 레인지 스캔을 했다. 그리고 rows가 1043이다. 1043개의 데이터에 액세스를 한 것이다.
       
      -> Filter: (users.department = 'Sales') (cost=520 rows=115) (actual time=0.151..8.31 rows=100 loops=1) -> Index range scan on users using idx_created_at over ('2024-06-29 06:30:15' <= created_at), with index condition: (users.created_at >= <cache>((now() - interval 3 day))) (cost=520 rows=1043) (actual time=0.0541..8.12 rows=1043 loops=1)
    6. idx_created_at 인덱스를 활용해 인덱스 레인지 스캔을 했다.
      1. → created_at >= DATE_SUB(NOW(), INTERVAL 3 DAY)을 만족하는 데이터를 뽑아낸다.
        → 이 때, 액세스한 데이터의 개수는 1043개(rows=1043)이다.
    7. 액세스한 1043개의 데이터 중 users.department = ‘Sales’을 만족하는 데이터를 필터링해온다.
      1. → 조건을 만족하는 데이터 개수는 100개이다. (rows=100)
       
       
      [department 컬럼을 기준으로 인덱스 생성]
      ALTER TABLE users DROP INDEX idx_created_at; # 기존 created_at 인덱스 삭제 CREATE INDEX idx_department ON users (department); # 성능 측정 SELECT * FROM users WHERE department = 'Sales' AND created_at >= DATE_SUB(NOW(), INTERVAL 3 DAY); # 실행 계획 EXPLAIN SELECT * FROM users WHERE department = 'Sales' AND created_at >= DATE_SUB(NOW(), INTERVAL 3 DAY); # 실행 계획 세부 내용 EXPLAIN ANALYZE SELECT * FROM users WHERE department = 'Sales' AND created_at >= DATE_SUB(NOW(), INTERVAL 3 DAY);
      notion image
      기존 200ms 응답 속도에서 140ms로 성능이 조금 향상됐다.
       
      notion image
      type이 ref인 걸로 봐서는 비고유 인덱스로 조회를 했다는 걸 알 수 있다. 여기까지는 크게 문제가 없는데 rows가 191,314(추정값)로 크게 잡혀있다. 즉, 데이터에 191,314번 액세스 했다는 뜻이다. 데이터 액세스를 많이 하면 할수록 시간이 오래 걸려 성능에 안 좋은 영향을 미친다. 이런 이유 때문에 created_at에 인덱스를 걸었을 때보다 성능이 덜 향상됐다. EXPLAIN ANALYZE를 통해서도 확인해보자.
       
      -> Filter: (users.created_at >= <cache>((now() - interval 3 day))) (cost=8900 rows=63765) (actual time=3.49..121 rows=114 loops=1) -> Index lookup on users using idx_department (department='Sales') (cost=8900 rows=191314) (actual time=0.32..111 rows=100000 loops=1)
    8. idx_department 인덱스를 활용해 department = ’Sales’를 만족시키는 데이터를 뽑아낸다.
      1. → 이 때, 액세스한 데이터의 개수는 100,000개이다. (rows=100000)
    9. 액세스한 100,000개의 데이터 중 users.created_at >= <cache>((now() - interval 3 day))을 만족하는 데이터를 필터링해온다.
      1. → 조건을 만족하는 데이터 개수는 114개이다.
       
       
      [created_at, department 컬럼 둘 다 인덱스 생성]
      # CREATE INDEX idx_department ON users (department); # 위에서 이미 추가함 CREATE INDEX idx_created_at ON users (created_at); # created_at 인덱스 추가 # 성능 측정 SELECT * FROM users WHERE department = 'Sales' AND created_at >= DATE_SUB(NOW(), INTERVAL 3 DAY); # 실행 계획 EXPLAIN SELECT * FROM users WHERE department = 'Sales' AND created_at >= DATE_SUB(NOW(), INTERVAL 3 DAY); # 실행 계획 세부 내용 EXPLAIN ANALYZE SELECT * FROM users WHERE department = 'Sales' AND created_at >= DATE_SUB(NOW(), INTERVAL 3 DAY);
      notion image
      기존 200ms 응답 속도에서 20ms로 줄어들었으니 약 10배 정도의 성능이 향상됐다.
       
      notion image
      하지만 실행 계획의 결과를 보면 key(실제 사용한 인덱스)에 idx_created_at 밖에 없다. 그 이유는 인덱스가 idx_department, idx_created_at 이렇게 둘 다 있지만 idx_created_at만 사용해서 조회하는 게 빠르다고 판단한 것이다.
       
      -> Filter: (users.department = 'Sales') (cost=515 rows=219) (actual time=0.411..10.3 rows=114 loops=1) -> Index range scan on users using idx_created_at over ('2024-06-29 07:11:17' <= created_at), with index condition: (users.created_at >= <cache>((now() - interval 3 day))) (cost=515 rows=1143) (actual time=0.123..10.1 rows=1143 loops=1)
      created_at 컬럼 인덱스만 생성한 결과와 동일하다.
       
      따라서 실질적으로 성능 향상에 큰 효과가 있는 created_at 컬럼에 한해서만 인덱스를 생성하는 게 효율적이다.
 
⭐
[이것만은 꼭 기억해두자!] 데이터 액세스(rows)를 크게 줄일 수 있는 컬럼은 중복 정도가 낮은 컬럼이다. 따라서 중복 정도가 낮은 컬럼을 골라서 인덱스를 생성해라.
 
 

✅ 멀티 컬럼 인덱스 고려하기

  1. (created_at, department) 멀티 컬럼 인덱스 생성
    1. ALTER TABLE users DROP INDEX idx_created_at; ALTER TABLE users DROP INDEX idx_department; CREATE INDEX idx_created_at_department ON users (created_at, department); SELECT * FROM users WHERE department = 'Sales' AND created_at >= DATE_SUB(NOW(), INTERVAL 3 DAY); # 실행 계획 EXPLAIN SELECT * FROM users WHERE department = 'Sales' AND created_at >= DATE_SUB(NOW(), INTERVAL 3 DAY);
      notion image
      약 30ms 정도 걸린다.
       
  1. (department, created_at) 멀티 컬럼 인덱스 생성
    1. ALTER TABLE users DROP INDEX idx_created_at_department; CREATE INDEX idx_department_created_at ON users (department, created_at); SELECT * FROM users WHERE department = 'Sales' AND created_at >= DATE_SUB(NOW(), INTERVAL 3 DAY); # 실행 계획 EXPLAIN SELECT * FROM users WHERE department = 'Sales' AND created_at >= DATE_SUB(NOW(), INTERVAL 3 DAY);
      notion image
      약 30ms 정도 걸린다.
 
위 2가지 멀티 컬럼 인덱스를 활용해서 설정해봤지만, created_at 인덱스만 걸었을 때와 크게 성능 차이가 없다. 이런 경우에는 굳이 멀티 컬럼 인덱스를 사용하지 않고 단일 컬럼에만 인덱스를 적용시키는 게 낫다.
 
⭐
[이것만은 꼭 기억해두자!] - ’단일 컬럼에 설정하는 일반 인덱스’를 설정했을 때와 ‘멀티 컬럼 인덱스를 설정했을 때’의 성능 차이가 별로 나지 않는다면, 멀티 컬럼 인덱스를 사용하지 말고 일반 인덱스를 활용하자.
 
 
 
 
 
 
 
author
category
MySQL
createdAt
series
비전공자도 이해할 수 있는 MySQL 성능 최적화 입문/실전 (SQL 튜닝편)
slug
type
series-footer
updatedAt
Jan 12, 2026 12:04 AM
📎
이 글은 비전공자도 이해할 수 있는 Docker 입문/실전 강의의 수업 자료 중 일부입니다.