SQL

[DB Optimization#15] HAVING 문 튜닝

j.d 2025. 4. 17. 21:22

SQL에서 집계 함수를 사용할 때 자주 쓰이는 함수가 있습니다.

 

바로 HAVIN문입니다.

 

하지만 무분별한 사용은 성능 저하를 초래할 수 있기 때문에, HAVING을 꼭 써야 하는 경우가 아니라면 WHERE문으로 대체하는 것이 더 바람직한 경우가 많습니다.

 

이번 글에서는 100만 건의 데이터를 기반으로 HAVING을 사용했을 때와 WHERE으로 대체했을 때의 성능 차이를 비교해 보며, 어떤 방식이 더 효율적인지 실습을 통해 확인해 보겠습니다.

 

 

실습

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100),
    age INT,
    department VARCHAR(100),
    salary INT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

SET SESSION cte_max_recursion_depth = 1000000;

INSERT INTO users (name, age, department, salary, 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')),
    FLOOR(1 + RAND() * 100),
    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,
    FLOOR(1 + RAND() * 1000000),
    TIMESTAMP(DATE_SUB(NOW(), INTERVAL FLOOR(RAND() * 3650) DAY) + INTERVAL FLOOR(RAND() * 86400) SECOND)
FROM cte;
CREATE INDEX idx_age ON users (age);
SELECT age, MAX(salary)
FROM users
GROUP BY age
HAVING age >= 20 AND age < 30;

 

age를 인덱스로 설정했음에도 불구하고 실행 시간이 약 1.605로 매우 느린 성능을 보여주고 있습니다.

 

자세한 원인 확인을 위해 EXPLAIN ANLAYZE를 통해 알아보겠습니다.

-> Filter: ((users.age >= 20) and (users.age < 30))  (cost=200263 rows=101) (actual time=348..1602 rows=10 loops=1)
    -> Group aggregate: max(users.salary)  (cost=200263 rows=101) (actual time=23.5..1602 rows=100 loops=1)
        -> Index scan on users using idx_age  (cost=100624 rows=996389) (actual time=0.457..1560 rows=1e+6 loops=1)

 

결과를 통해 첫 번째 Index scan이 이루어질 때 사용된 idx_age에는 id와 age만 존재하는데 이후 group aggregate에 사용된 salary 데이터를 위해 전체 데이터를 스캔해야 하는 현상이 발생되어 속도가 느려진 것으로 판단됩니다.

 

 

 

해결 방법

SELECT age, MAX(salary)
FROM users
WHERE age >= 20 AND age < 30
GROUP BY age;

결과 테이블(좌: 변경 전, 우: 변경 후)

 

실행 결과 테이블은 HAVING 문이 적용된 쿼리와 동일하게 나왔지만, 실행 시간이 0.188로 매우 좋아졌음을 알 수 있습니다.

 

EXPLAIN ANAYLZE를 이유를 찾아보겠습니다.

-> Group aggregate: max(users.salary)  (cost=105781 rows=101) (actual time=17.3..163 rows=10 loops=1)
    -> Index range scan on users using idx_age over (20 <= age < 30), with index condition: ((users.age >= 20) and (users.age < 30))  (cost=86548 rows=192328) (actual time=0.29..160 rows=100407 loops=1)

맨 처음 Index range scan에서 age 인덱스를 통해 먼저 필터링된 후 데이터를 사용하기 때문에 시간이 절약된 것을 알 수 있습니다.

 

따라서 HAVING은 반드시 필요한 경우에만 사용해야 하며, 조건 필터링이 가능하다면 WHERE절로 먼저 처리하는 것이 성능 상 훨씬 효율적입니다.

 

GROUP BY 전에 데이터를 줄이면 MySQL이 처리할 연산량이 줄어들기 때문입니다.

HAVING은 그룹핑 이후의 필터링이므로, 인덱스를 제대로 활용하지 못하는 경우가 많습니다.