데이터베이스, ORM

[MYSQL] Like 연산자와 %,_ 와일드카드를 이용한 검색 성능 개선하기(InnoDB)

SparkIT 2025. 10. 25. 14:24

데이터베이스를 다루다 보면 “부분 검색”이 꼭 필요할 때가 있습니다.
예를 들어 이름이 '김'으로 시작하거나 포함된 사용자를 찾고 싶을 때 다음과 같은 쿼리를 자주 작성하죠.

SELECT * FROM users WHERE name LIKE '%김%';

하지만 이런 쿼리는 굉장히 느리게 동작하여 시스템에 무리를 일으키기도 합니다. 인덱스도 걸어놨는데, 왜 느릴까요?
이번 글에서는 MySQL의 LIKE 연산자와 와일들카드 그리고 인덱스가 어떻게 동작하는지 이해하고 이를 통해 어떻게 검색 성능을 개선할 수 있는지 여러 방법에 대해 고민해보겠습니다.


기본 용어 설명

먼저 기본부터 짚고 갑시다.
LIKE는 MySQL에서 비교 연산자(Comparison Operator) 중 하나입니다.
'=' 혹은 '>' 같은 비교 연사자처럼 두 값을 비교해서 TRUE/FALSE를 반환합니다.

SELECT * FROM users WHERE name LIKE '김%';

이 쿼리는 "김"으로 시작하는 문자열을 찾는 연산입니다.

 

LIKE와 함께 자주 쓰이는 특수 문자가 있습니다. 바로 와일드 카드 문자입니다.

와일드카드 예시

% 0개 이상의 임의의 문자 '김%' → "김", "김준", "김민수"
_ 정확히 1개의 임의의 문자 '김_수' → "김민수", "김진수"

문자열 패턴을 만들 때 이 둘을 조합해 다양한 조건을 걸 수 있습니다.


B+Tree 구조는 문자열에 대한 인덱스를 생성할때 문자열의 가장 왼쪽부터 이용해 정렬한다

데이터베이스 조회 속도에 가장 큰 영향을 주는 것은 '인덱스'라고 말할 수 있습니다.

이런 인덱스의 구조부터 이해하고 갑시다.

 

우선 저는 MySQL InnoDB 엔진을 사용합니다. 해당 엔진에서는 B+Tree 기반 인덱스 구조를 사용합니다.

B+Tree 인덱스는 문자열을 정렬 가능한 값으로 바꾼 뒤 트리 형태로 만듭니다.

B+Tree 구조에 대한 간단한 예시입니다. 다음과 같은 이름 데이터가 5개 존재한다고 가정합시다.

김강민
김정구
이민혁
백호민
최호찬

그리고 이 데이터들은 'utf8mb4_general_ci' 으로 저장되어있다고 가정합니다. (utf8mb4_general_ci는 유니코드값을 기준으로 비교됩니다)

 

다음은 각 이름에 대한 유니코드값 정보입니다.

김강민 --> [44608, 44053, 48188]
김정구 --> [44608, 51221, 44428]
이민혁 --> [51060, 48188, 54849]
백호민 --> [48353, 54840, 48188]
최호찬 --> [52572, 54840, 52268]

이는 이름을 이루고 있는 각각의 문자에 대한 유니코드값을 배열로 표현한 것입니다. 예로 '김강민'의 '김'이 44608, '강'이 44053, '민'이 48188이라는 유니코드값을 가진 것이죠.

이 부분이 중요합니다!

B+Tree 구조에서는 문자열 정보를 저장할 때 유니코드값 배열을 왼쪽에서부터 비교하고 차례대로 정렬됩니다.

 

즉, 5개의 이름 데이터가 아래와 같이 정렬된다는 것이죠.

                          [EA B9 80 ... 김*] ──┬── [EB B0 ... 백*] ──┬── [EC 9D ... 이*]
                                              │                     │
                                              ▼                     ▼
                              [김강민, 김정구]          [백호민]          [이민혁, 최호찬]

 

 

여기서 두 가지 경우를 비교해봅시다.

  • 경우1) '김'으로 시작하는 이름 찾기
  • 경우2) 중간에 '호'가 들어가는 이름 찾기

 

첫번째 경우에서는 루트에서 '김'으로 시작하는 브랜치로 바로 이동할 수 있습니다. 이를 통해 '김강민'을 알아낼 수 있죠. 그리고 리프 노드들은 모두 양방향 연결되어있기 때문에 '김강민' --> '김정구' --> '백호민' 순서로 이동하면서 검사합니다. 이때 '백호민'이라는 데이터는 조건을 만족하지 않기에 '김강민', '김정구' 데이터만 반환됩니다. 이 과정에서는 조건에 맞는 첫번째 데이터를 찾기 굉장히 쉽고, 조건을 만족하지 않을 때까지 오른쪽으로 계속 이동하기만 되기 때문에 빠르게 처리됩니다.

두번째 경우에서는 루트에서 특정 브랜치로 이동 자체가 불가합니다. 이름 중간에 '호'가 있는 데이터는 여러 브랜치에 있을 수 있기 때문이죠. 그래서 결국 모든 데이터를 순환해야합니다. 이 과정은 모든 데이터를 탐색하게 되므로 느릴 수 밖에 없죠.

 

 


그럼 어떻게 조회(검색) 성능을 높일까?

상황에 따라 여러가지 방식을 활용할 수 있습니다.

방법 1 ) LIKE 연산자 + '검색어%' 형식으로 제한하기

위에서 설명했듯이 '검색어%' 형식처럼 접두사 검색은 인덱스를 활용할 수 있기에 성능상 이점이 있습니다.

그러므로 시작하는 문자열을 모두 적어야 제대로 된 검색이 가능하도록 UX를 설계하는 것이 첫번째 방법입니다.

 

방법 2 ) FULLTEXT INDEX 활용하기

InnoDB에서는 FULLTEXT 인덱스를 지원하는데요.

이는 단어 단위로 토큰화하여 검색할 수 있는 인덱스 구조입니다.

예를 들어 '안녕하세요. 저는 홍길동이라고 합니다' 라는 문자열을 저장한다면 '안녕하세요', '저는', '홍길동이라고', '합니다' 로 나누어 검색할 수 있다는 것입니다. '저는'이라는 단어를 검색하면 해당 문자열을 빠르게 조회할 수 있습니다.

하지만 이런 인덱스는 여러 단어를 구성된 문장을 저장할 때 사용되면 좋은 구조입니다.

위에서 예시로 들었던 이름 검색같은 경우에서는 이점이 없습니다.

 

방법 3 ) REVERSE INDEX 활용하기

문자열을 거꾸로 뒤집고, 이에 대한 인덱스를 생성하는 것도 생각해볼 수 있는 방법 중 하나입니다.

예를 들어 한국어로 된 이름을 검색할 때, 성을 빼고 검색하는 경우가 많을 수 있습니다.('김정구' 검색 시 '정구'라는 이름만 가지고 검색)

이런 경우에는 가장 마지막 문자열로부터 역으로 인덱스를 사용하면 위에서 설명했던 B+Tree 구조의 이점을 그대로 이용할 수 있어 성능 향상에 도움이 될 수 있습니다.

 

 


마무리

결국 인덱스를 활용한 조회의 핵심은 “정렬된 순서를 얼마나 잘 활용할 수 있느냐”에 있습니다.

LIKE, FULLTEXT, REVERSE INDEX 모두 이 정렬 특성을 어떤 방향으로 사용할지를 달리한 전략일 뿐이죠.

검색 성능을 높이는 일은 결국 기술의 문제가 아니라, 데이터와 검색 의도를 정확히 이해하는 일에서 출발합니다.