
데이터베이스 성능 최적화는 거창한 인프라 증설보다 우선시 되야할것이 바로 '나쁜 쿼리 습관'을 버리는 것에서 시작됩니다.
여기서 말하는 나쁜 습관이란 쿼리를 작성할 때 인덱스를 만들어두고도 제대로 활용하지 못하는 대표적인 실수 6가지와 그 해결책을 분석과 함께 정리했습니다.
0. 실습을 위한 테이블 구조
먼저 예시 쿼리들을 테스트해 볼 수 있는 환경입니다.
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50),
email VARCHAR(100),
dept_id INT,
status VARCHAR(20),
created_at DATETIME,
-- 복합 인덱스 설정
INDEX idx_dept_status (dept_id, status),
INDEX idx_username (username),
INDEX idx_created_at (created_at)
);
-- 테스트 데이터 삽입
INSERT INTO users (username, email, dept_id, status, created_at) VALUES
('admin', 'admin@example.com', 10, 'active', '2025-01-01 10:00:00'),
('iron_man', 'tony@stark.com', 20, 'inactive', '2025-05-15 14:00:00'),
('spider_man', 'peter@parker.com', 10, 'active', '2025-12-20 09:30:00');
1. SELECT * 대신 필요한 컬럼만 명시하기
많은 개발자가 편의상 * (애스터리스크,와일드카드 등으로 부름)를 사용하지만, 이는 I/O 비용을 불필요하게 증가시킵니다.
위 * 는 일회성 조회나 작업을 위할 때는 사용하지 않아도 되지만 코드나 특정 다른곳에서 반복적으로 사용할 때는 가장 피해야할
기초적인 안티패턴입니다.
- 나쁜 예: SELECT * FROM users WHERE status = 'active';
- 좋은 예: SELECT id, username FROM users WHERE status = 'active';
- Why? 간략히 말해 필요한 컬럼만 조회하면 네트워크 전송량이 줄어듭니다. 특히 조회하려는 컬럼이 모두 인덱스에 포함되어 있다면, 실제 데이터 페이지를 열지 않고 인덱스만으로 결과를 반환하는 커버링 인덱스(Covering Index)가 작동하여 압도적으로 빨라집니다.
2. WHERE절 좌변 가공 금지 (인덱스 무력화)
인덱스 컬럼을 함수로 감싸거나 연산을 가하면 MySQL 옵티마이저는 인덱스 트리를 탈 수 없습니다. 왜냐면 WHERE 절의 컬럼에 ()를 붙여서 가공을 한순간 해당 모든컬럼에 그 함수를 적용하고 그 이후에 조건문과 비교하기 때문입니다.
따라서 연산은 항상 WHERE절의 우측에서 작성해야합니다.
- 나쁜 예: WHERE YEAR(created_at) = 2025;
- 좋은 예: WHERE created_at >= '2025-01-01' AND created_at <= '2025-12-31';
- Why? 인덱스는 '가공되지 않은 원본 데이터'를 기준으로 정렬되어 있습니다. YEAR()와 같은 함수를 씌우는 순간 정렬 순서가 무의미해져, 결국 전체 데이터를 하나하나 YEAR를 적용시키고 2025가 맞는지 확인하는 Full Table Scan이 발생합니다.
따라서 2025를 확인하고 싶다면 BETWEEN이나 위처럼 날짜를 지정하는 연산으로 확인하는 것이 바람직합니다.
3. 앞부분 와일드카드 사용 자제 (LIKE '%단어')
문자열 검색 시 시작 지점을 알 수 없는 검색은 인덱스를 활용하지 못합니다. %를 뒤에 붙이면 시작지점이 특정되기 때문에
B-TREE인덱스가 제대로 시작지점을 체크해서 인덱싱을 할수 있는데 맨앞에 %를 붙여버리면 시작지점을 찾을 수 없기에 가급적이면 %를 앞이 아닌 뒤에 쓰는 형식의 패턴의 쿼리를 작성해야합니다.
특정 단어로 끝나는 검색(%man)이 잦다면, 데이터를 거꾸로 뒤집어서 저장하는 컬럼(nam%)을 만들고 여기에 인덱스를 거는 편법인 역방향 인덱스가 있습니다.
- 나쁜 예: WHERE username LIKE '%man';
- 좋은 예: WHERE username LIKE 'iron%';
- Why? 인덱스는 사전(Dictionary)과 같습니다. 'i'로 시작하는 단어는 바로 찾을 수 있지만, 'n'으로 끝나는 단어를 찾으려면 사전의 첫 페이지부터 끝까지 다 뒤져야 하는 것과 같은 원리입니다.
4. 묵시적 형변환 (Implicit Type Conversion)
컬럼의 데이터 타입과 비교하는 값의 타입이 다를 때 발생하는 문제입니다. 비교하려는 두 데이터의 타입이 일치하지 않을 때, MySQL은 내부적으로 우선순위가 높은 타입으로 데이터를 자동 변환합니다. 이것이 바로 '묵시적 형변환'입니다.
이는 2번의 좌변가공 금지와 똑같습니다.
- 나쁜 예: WHERE username = 12345; (문자열 컬럼을 숫자로 조회)
- 좋은 예: WHERE username = '12345';
- Why? 타입이 다르면 MySQL 내부에서 타입을 변환하는 연산이 발생하며, 이는 2번 항목(좌변 가공)과 동일하게 인덱스 성능을 마비시킵니다.
MySQL에서 문자열과 숫자를 비교하면, 문자열 컬럼을 숫자로 변환하여 비교하는 것이 우선순위입니다.
즉, 내부적으로는 WHERE CAST(username AS UNSIGNED) = 12345와 같은 연산이 모든 행(Row)에 대해 실행됩니다. 2번에서 언급한 '좌변 가공' 현상이 눈에 보이지 않게 발생하는 셈입니다.
또 인덱스는 username이라는 문자열을 기준으로 정렬되어 있는데 이를 숫자로 변환하는 순간 정렬 순서가 완전히 뒤바뀌기 때문에 인덱스 트리를 활용한 검색이 불가능해지고, Full Table Scan으로 이어집니다.
묵시적 형변환은 성능만 느려지는 게 아닙니다. 제 경험을 말씀드리면 다른 개발자가 문자형식의 컬럼을 조회할때 ' 를 빼먹고 코드를 작업해서 12345를 조회했는데 '12345-temp' 같은 엉뚱한 데이터까지 함께 조회되는 현상이 발생해서 고생했던 경험이 있습니다.
즉 묵시적 형변환은 데이터 정합성 오류를 야기하며 예상치 못한 버그를 막으려면 반드시 타입을 일치시켜야 합니다."
5. 복합 인덱스의 '가장 왼쪽 일치' 원칙 준수
복합 인덱스는 첫 번째 컬럼으로 먼저 정렬되고, 그 안에서 두 번째 컬럼으로 정렬되는 계층적 구조를 가집니다. 복합 인덱스는 나열된 컬럼의 순서대로 정렬됩니다. 이 순서를 건너뛰면 안 됩니다.
- 상황: 인덱스가 (dept_id, status) 순으로 생성됨.
- 나쁜 예: SELECT * FROM users WHERE status = 'active';
- 좋은 예: SELECT * FROM users WHERE dept_id = 10 AND status = 'active';
- Why? 인덱스는 왼쪽에서 오른쪽 방향으로만 탐색이 가능합니다. 첫 번째 컬럼인 dept_id가 조건에 없다면, MySQL 옵티마이저는 이 인덱스를 효율적이지 않다고 판단하여 사용하지 않거나(Full Table Scan), 인덱스 전체를 다 읽는 비효율적인 방식을 택합니다. 이건 전화번호부가 [성, 이름] 순으로 정렬되어 있을 때, '이름'만으로 사람을 찾을 수 없는 것과 같습니다.
6. OR 조건 대신 UNION ALL 활용
OR 조건은 데이터베이스가 어떤 인덱스를 선택해야 할지 어렵게 만들며, 성능 예측이 가장 힘든 패턴 중 하나입니다.
따라서 MYSQL의 옵티마이저는 OR 조건을 처리하기 위해 두 개의 인덱스를 각각 읽어 결과를 합치는 'Index Merge'
를 사용하기도 하지만 이는 비용이 비싸서 그냥 FULL SCAN을 사용하는 경우가 발생합니다.
- 나쁜 예: SELECT * FROM users WHERE dept_id = 10 OR status = 'inactive';
- 좋은 예:
SELECT * FROM users WHERE dept_id = 10
UNION ALL
SELECT * FROM users WHERE status = 'inactive'; - Why? OR를 사용하면 옵티마이저가 어떤 인덱스를 쓸지 혼란을 겪거나 인덱스 병합(Index Merge)을 시도하며 성능이 들쭉날쭉해집니다. UNION ALL은 각 쿼리가 명확히 인덱스를 타게 한 뒤 결과만 합치므로 실행 계획이 매우 안정적입니다.
첫 번째 쿼리는 dept_id 인덱스를, 두 번째 쿼리는 status 인덱스를 타도록 확정지어 줄 수 있습니다. 결과적으로 옵티마이저의 변심 없이 항상 일정한 성능을 보장받을 수 있습니다. - 단 위의 UNION ALL 대신 OR을 사용하는게 부하나 비용이 덜 걸리수 있을 수 있으니 항상 상황에 맞게 사용해야합니다.
※ 성능 확인을 위한 팁: EXPLAIN
내 쿼리가 인덱스를 잘 타고 있는지 궁금하다면 쿼리 맨 앞에 EXPLAIN을 붙여보서 사용하여 성능을 볼수 있습니다.
자세한 내용은 아래 포스팅을 참고하면 됩니다!
https://gapal.tistory.com/60
MySQL EXPLAIN TYPE 완벽 정리: ALL, index, range, ref 차이 쉽게 이해하기
MY SQL의 쿼리 성능을 최적화 하려면 매우 많은 부분을 생각해야합니다. 단순히 인덱싱만 한다고 해서 해결되는게 아닐경우도 있는데요. 그런 최적화를 위해서 기본적으로 SELECT 문의 성능을 확인
gapal.tistory.com
'DB > Mysql' 카테고리의 다른 글
| [MySQL 심층 탐구] 가상 테이블 'VIEW', 왜 사용하고 어떻게 설계해야 하는가? (0) | 2025.12.31 |
|---|---|
| 서브쿼리를 쓰지 말아야 할 기술적 이유와 근본적 설계 (0) | 2025.12.30 |
| MySQL 피벗(Pivot) 완벽 가이드: CASE 문 사용하기 (0) | 2025.12.21 |
| MySQL 운영 DB에서 안전하게 트리거 우회하기 (내 세션만 일시정지하는 법) (1) | 2025.12.19 |
| MySQL: 이모지 삽입 시 Incorrect string value 오류 해결 (0) | 2025.11.17 |
