Skip to content
isdnetworks
Go back

검색만을 위한 테이블이 따로 있었다

DB 설계서에 테이블이 하나 더 있었다.

search_user (검색용 유저 이메일, 닉네임 정보 테이블)

컬럼이 넷이다.

seq
user_email
user_nick
reg_date

전부 유저 테이블에도 있는 값이다.

Table of contents

Open Table of contents

설명이 붙어 있었다

seq 컬럼 설명이 이랬다.

user_info 테이블의 seq 값과 같은 값이다
(search_user 테이블의 seq 값은 user_info 테이블의 seq 값과 동기화된다)

그리고 이런 문장도 있었다.

user_info 테이블에 신규 유저가 등록된 후
user_info 테이블에 생성된 seq 값을
search_user 테이블의 seq 컬럼에 저장시킨다

본체에 넣고 나서 여기에도 넣는다는 것이다.

왜 나눴는지 궁금했다

같은 값을 두 곳에 두면 어긋날 위험이 생기므로 보통은 피한다. 굳이 나눈 이유가 있을 것 같아 두 테이블을 나란히 놓아 봤다.

user_info
seq, user_id, user_email, user_nick, user_level, user_exp,
user_coin, user_total_payment, user_state, bigwin_coin,
jackpot_coin, bigwin_multiple, jackpot_multiple, max_coin,
is_like, daily_date, last_visit, reg_date, ...
search_user
seq, user_email, user_nick, reg_date

user_info 는 스무 개 가까이 되고 search_user 는 넷이다. 유저 테이블 한 행이 훨씬 크고 검색 테이블은 가볍다.

그러면 같은 100만 행이어도 읽어야 할 양이 다르다. InnoDB 는 16KB 페이지 단위로 읽으니 행이 작을수록 한 페이지에 더 많이 들어가고 같은 행 수를 훑어도 읽는 페이지가 몇 분의 일이 된다.

검색이 어떤 모양인가

관리자 화면에서 유저를 찾는다면 이런 식일 것이다.

WHERE user_email LIKE '%검색어%' OR user_nick LIKE '%검색어%'

앞에 와일드카드가 붙으면 인덱스를 못 탄다. 인덱스는 앞에서부터 맞춰 가는 구조라 % 가 앞에 있으면 소용이 없고 EXPLAINtypeALL 로 나온다. 그러면 행이 작을수록 빠르다.

유저 테이블에도 이메일과 닉네임에 인덱스가 걸려 있었다.

user_email   index
user_nick    index

그런데 부분 일치 검색이면 그 인덱스가 안 쓰인다. 인덱스가 있어도 소용없는 검색이 있다는 것을 이때 알았다.

다른 길이 없지는 않았다. 검색에 쓰는 컬럼만으로 복합 인덱스를 걸면 Using index 로 표를 안 읽고 인덱스만 훑으므로 표를 나누지 않고도 읽는 양이 준다. InnoDB 의 FULLTEXTngram 파서를 붙이는 길도 그 무렵에는 열려 있었는데 설계서에는 둘 다 없었다.

갱신 성격도 달랐다

또 하나 떠오른 것이 있다. 유저 테이블은 코인이 바뀌고 경험치가 오르고 마지막 접속이 갱신되면서 게임 한 판마다 바뀌는 컬럼이 있다.

검색 테이블은 이메일과 닉네임이라 거의 안 바뀐다. 쓰기가 잦은 쪽과 거의 읽기만 하는 쪽으로 갈리니 성격이 다른 컬럼을 나눈 셈이 된다.

검증 — 대가와 양방향 대조

대신 잃는 것이 있다.

등록할 때 두 번 써야 한다
닉네임이 바뀌면 두 곳을 바꿔야 한다
한쪽만 되면 어긋난다

설명에 동기화된다고 적혀 있는 것이 이 뜻이고 동기화를 애플리케이션이 책임진다. TRIGGER 로 묶는 길도 있지만 그것은 그것대로 숨은 동작이 된다.

어긋나면 어떤 일이 생길지도 생각해 봤다. 검색에만 있고 본체에 없으면 검색은 되는데 상세가 없고 본체에만 있고 검색에 없으면 존재하는데 안 찾아진다.

두 번째가 더 안 보인다. 유저는 있는데 관리자가 못 찾으니 버그로 인식되기까지 오래 걸릴 것 같다.

SELECT seq FROM user_info
 WHERE seq NOT IN (SELECT seq FROM search_user);
SELECT seq FROM search_user
 WHERE seq NOT IN (SELECT seq FROM user_info);

양쪽 다 봐야 한다. 한 방향만 돌리고 0건이 나오면 다른 방향의 누락은 그대로 남는다.

이 문제를 다르게 풀 수도 있었을 것 같다. 전문 검색 기능을 쓰거나 검색 결과를 캐시에 담아 두거나 뷰로 두는 방법이 떠올랐는데 뷰는 실제로 안 나뉘니까 성능 이득이 없을 것이고 전문 검색은 이 시점에 어떻게 쓰는지 잘 몰랐다. 나중에 다른 방법을 알게 되면 다시 볼 부분이다.

정리


Share this post on:

Previous Post
어느 단위의 상태인지 안 적혀 있었다
Next Post
볼 때마다 같은 구조를 다시 파악했다