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
createdAt
series
비전공자도 이해할 수 있는 MySQL 성능 최적화 입문/실전 (SQL 튜닝편)
slug
type
series-footer
updatedAt
Dec 6, 2025 12:47 AM
📎
이 글은 비전공자도 이해할 수 있는 Docker 입문/실전 강의의 수업 자료 중 일부입니다.