두 테이블의 기본키를 나란히 봤다.
user_info seq int primary key auto_increment: o
search_user seq int primary key auto_increment: (없음)
이름도 같고 형도 같은데 한쪽만 자동 증가다.
Table of contents
Open Table of contents
자동 증가가 꺼져 있었다
search_user 의 seq 설명이 이랬다.
시퀀스, user_info 테이블의 seq 값과 같은 값이다
(search_user 테이블의 seq 값은 user_info 테이블의 seq 값과 동기화된다)
같은 번호를 쓴다. 여기서 자동 증가가 꺼진 이유가 이해가 됐다.
AUTO_INCREMENT 를 켜 두면 그 표가 INSERT 때마다 자기 번호를 스스로 매긴다. 그러면 양쪽이 1과 2와 3으로 우연히 같아 보이지만 다른 번호다.
한쪽에서 행이 하나 지워지면 어긋난다. user_info 가 1과 2와 4를 갖고 search_user 가 1과 2와 3을 가지면 3번이 서로 다른 유저를 가리킨다.
받는 쪽은 번호를 스스로 만들면 안 되고 넘겨받아야 한다. 속성을 빼 두는 것이 그 규칙을 구조로 만드는 방법이었다.
본체 다음이라는 순서
설계서에 절차가 적혀 있었다.
user_info 테이블에 신규 유저가 등록된 후
user_info 테이블에 생성된 seq 값을
search_user 테이블의 seq 컬럼에 저장시킨다
순서가 정해져 있다. 반대로 하면 검색 쪽에서 번호를 모르니 못 넣고 본체 쪽은 번호가 나오니 넣을 수 있다.
user_info 가 번호를 만드는 쪽이라 먼저다.
방금 넣은 번호를 얻었다
본체에 넣고 나서 번호를 어떻게 아는지 생각해 봤다.
INSERT INTO user_info (...) VALUES (...);
SELECT LAST_INSERT_ID();
LAST_INSERT_ID() 가 방금 넣은 행의 번호를 준다. 이것을 알기 전에는 이렇게 할 뻔했다.
SELECT MAX(seq) FROM user_info;
MAX(seq) 는 동시에 두 명이 가입하면 틀린다. 남의 번호를 가져올 수 있다.
매뉴얼은 LAST_INSERT_ID() 값이 연결마다 따로 유지되고 다른 클라이언트가 자기 번호를 만들어도 영향을 안 받는다고 적고 있다. 잠금이나 트랜잭션 없이도 자기 번호만 정확히 돌아온다는 뜻이다.
다만 한 INSERT 로 여러 행을 넣으면 첫 행의 값만 돌아온다. 여러 건을 한꺼번에 넣는 자리에서는 이 성질에 걸릴 수 있어서 한 건씩 넣고 받는 편이 안전했다.
기본키가 같으면 조인이 쉽다
같은 방식이 다른 곳에도 있었다. 무료스핀 슬롯이 일반 슬롯의 seq 를 가리키고 코인 로그가 user_id 를 갖는다.
다른 테이블의 키를 갖는 것은 참조이고 같은 값을 기본키로 쓰는 것은 1대1 대응인데 search_user 는 뒤쪽이다.
1대1이면 한 테이블에 다 넣어도 되지 않나 싶은데 행 크기 때문으로 보인다. 검색할 때 훑는 양이 준다. 같은 대상을 두 테이블로 나눈 것이지 다른 대상이 아니다.
이렇게 두면 조인이 단순하다.
SELECT * FROM search_user s
JOIN user_info u ON u.seq = s.seq
WHERE s.user_nick LIKE '%검색어%';
작은 테이블에서 찾고 큰 테이블에서 상세를 가져온다. 번호가 달랐으면 매핑 테이블이 하나 더 필요했을 것이고 양쪽이 기본키라 인덱스도 따로 안 만들어도 된다.
판단 기준 — 어디까지 복제할지
유지에서 걸리는 것이 있다. 유저를 지우면 양쪽을 다 지워야 한다.
본체만 지우면 검색에는 남아 있어서 없는 유저가 검색되고 검색만 지우면 본체에 있는데 안 찾아진다. FOREIGN KEY 를 걸면 ON DELETE CASCADE 로 InnoDB 가 대신 처리할 자리인데 이 설계는 외래키를 안 쓰므로 애플리케이션이 둘 다 지워야 한다.
그런데 게임 서비스에서 유저를 정말 지우는지는 모르겠다. 유저 테이블에 user_state 가 있고 1은 일반이고 2는 거부다. 지우는 대신 상태를 바꾸는 것으로 보이며 그러면 삭제 문제는 거의 안 생길 것 같다.
다만 search_user 에는 user_state 가 없다. 검색 결과에 거부된 유저도 나오고 상세를 봐야 상태를 안다.
SELECT * FROM search_user s JOIN user_info u ON u.seq = s.seq
WHERE s.user_nick LIKE '%검색어%' AND u.user_state = 1;
검색 단계에서 거르려면 조인해야 하는데 조인 조건이 붙으면 작은 테이블만 훑는 이득이 줄어들 것 같다.
여기서 생각이 하나 남았다. 적게 복제하면 행이 작은 대신 조건을 걸려면 조인해야 하고 많이 복제하면 조인 없이 걸러지는 대신 행이 커지고 동기화할 것이 는다.
자주 걸리는 조건이면 복제하는 것이 나을 것 같은데 그만큼 동기화할 컬럼이 는다.
정리
- 두 테이블이 같은 기본키 값을 쓴다
- 받는 쪽은 자동 증가가 꺼져 있고 켜 두면 각자 번호를 매겨 어긋난다
- 속성을 빼 두는 것이 그 규칙을 구조로 만드는 방법이다
- 본체에 먼저 넣고 그 번호를 받아서 넣는다
- 방금 넣은 번호는
LAST_INSERT_ID()로 얻고 연결마다 따로 유지된다 MAX(seq)로 조회하면 동시 가입에서 남의 번호가 온다- 한
INSERT로 여러 행을 넣으면 첫 행의 값만 돌아온다 - 기본키가 같으면 조인이 단순하고 매핑 테이블이 필요 없다
- 외래키가 없으면 삭제 시 양쪽을 애플리케이션이 처리해야 한다
- 상태 컬럼으로 거부를 표시하면 삭제가 드물어 문제가 적어 보인다
- 검색 쪽에 조건 컬럼이 없으면 조인해야 걸러진다
- 어느 컬럼까지 복제할지가 판단이다