인덱스 없이 조회 성능을 올리는 방법

미리 집계 하기

  • 설문 조사 기능이 있다
  • 각 설문조사마다 좋아요를 할 수 있고 설문 조사 목록을 보여줄 때 답변 수와 좋아요수를 표시한다
  • 이를 위해 survey, answer, liked 테이블이 존재한다.
  • 목록을 표시할 때 설문에 답한 회원 수와 좋아요를 한 회원 수를 표시한다는 요건을 위한 쿼리
SELECT s.id, s.subject,
	(SELECT COUNT(*) FROM answer a WHERE a.surveyId = s.id) as answerCnt,
	(SELECT COUNT(*) FROM liked a WHERE l.surveyId = s.id) as likeCnt
FROM survey s
ORDER BY id DESC
LIMIT 30;
  • 논리적으로 정확하지만 성능에 이슈가 존재할 수 있다
  • 30개의 설문이 있을 때 설문마다 평균 답변자 수가 10만명이고 좋아요를 한 회원 수가 1만이라 가정해보자. 위 쿼리 실행시 다음과 같이 실행된다.
    • 목록 조회
    • 답변자 수 세는 쿼리 30번: 각 쿼리는 10만개 센다
    • 좋아요 수 세는 쿼리 30번: 각 쿼리 1만개 센다
  • 합치면 논리적으로 61번 쿼리가 실행된다
  • 쿼리 시간: 0.01 + 0.1 * 30 + 0.05 * 30 = 4.51
  • 4.51 초는 온라인 서비스에서 매우 긴 시간
  • 게다가 트래픽이 몰리면 더욱 조회 시간이 느려질것이다.

  • 이를 개선하려면 집계 쿼리를 안쓰고 미리 계산해서 별도 컬럼에 저장해 두면됨
  • 이 예에서는 servey 테이블에 답변자 수와 좋아요 수를 저장할 컬럼 추가하고, 이 컬럼에 값을 계산해 넣는 방식으로 구현
  • 다음과 같은 쿼리를 쓰는거지
-- answer table에 답변 추가
INSERT INTO answer VALUES (...)
 
-- servey table의 answerCnt 값을 1 증가시킨다.
UPDATE survey SET answerCnt = answerCnt + 1 
WHERE surveyId = 아이디;
 
-- 좋아요도 비슷. 취소했다면 likedCnt - 1 하면됨
DELETE FROM liked WHERE answerId = 설문ID AND memberId = 회원ID;
UPDATE servey SET likedCnt = likedCnt - 1 WHERE surveyId = 설문ID

페이지 기준 목록 조회 대신 ID 기준 목록 조회 방식 사용하기

  • 게시글 데이터가 10만개 있다 가정
  • 화면에 10개씩 보여주면 전체 페이지수는 1만개가 된다
  • 첫 페이지 읽기 위한 쿼리는 다음과 같다
SELECT id, subject, writer, regdt
FROM article
ORDER BY id DESC
LIMIT 10 OFFSET 0;
  • 다음은 마지막 1만 번째 페이지를 읽어오는 쿼리
SELECT id, subject, writer, regdt
FROM article
ORDER BY id DESC
LIMIT 10 OFFSET 99990;
  • 이 쿼리를 실행시 99,991번째 id부터 바로 조회하면 좋겠지만...
  • DB는 어떤 id가 99.991 번째인지 모른다
  • 그래서 역순으로 id를 99,990개 세고 나서 10개 데이터를 조회
  • 데이터를 세는 시간만큼 실행시간이 증가하는 것

  • 인덱스에 포함되어 있지 않은 칼럼은 더 느리다
  • 다음 쿼리는 삭제되지 않은 게시글 목록을 조회한다.
SELECT id, subject, writer, regdt
FROM article
WHERE deleted = false
ORDER BY id DESC
LIMIT 10 OFFSET 10000;
  • deleted 칼럼이 false인지 비교할 목적으로 데이터를 읽어오는 시간을 줄이기 위해, deleted 칼럼을 인덱스로 사용할 수 도 있다
  • 하지만 여전히 지정한 오프셋만큼 데이터를 세는시간이 남는다.

  • 지정한 오프셋으로 이동하기 위해 데이터를 세는 시간을 줄이는 방법은 특정 ID를 기준으로 조회하는 것!
  • 예를 들어 마지막으로 다음 쿼리로 조회한 데이터의 id가 9985라 하면
SELECT * FROM article
WHERE deleted = false;
ORDER BY id DESC
LIMIT 10;
  • 다음 10개를 읽을때는 앞서 읽어온 마지막 id를 사용해서 조회하면된다
SELECT * FROM article
WHERE id < 9985 AND deleted = false;
ORDER BY id DESC
LIMIT 10;
  • id는 인덱스이므로 바로 9984 찾는다
  • 오프셋을 썻을때의 데이터 세는 시간이 생략되는 것!

  • 프론트 개발자가 다음에 읽어올 데이터 존재하는지 알려주는 속성을 응답결과에 포함시켜달라하면
  • 1개만더 읽어 판단하면 된다
  • 예를 들어 10개를 주면될때는 11개로 읽고 11개면 다음에 읽을게 존재하는것
  • 이건 비즈니스로직으로 판단하면될 듯
SELECT * FROM article
WHERE id < 1001 AND deleted = false
ORDER BY id DESC
LIMIT 11;

조회 범위를 시간 기준으로 제한하기

  • 조회 성능 개선 방법중 하나는 조회 범위를 시간 기준으로 제한하는 것
  • 최신 데이터 위주로 조회하게 기능을 변경하면 DB성능 또한 향상
  • DB는 성능향상을 위해 메모리 캐시를 쓴다
  • 조회가 발생하면 이를 메모리에 캐시해 다음 동일 요청시 더 빨리 응답
  • 일반적으로 최신 데이터가 더 많이 조회되기에 캐시에 최신 데이터가 적재될 확률이 높다

전체 개수 세지 않기

  • 목록을 표시하는 기능은 전체 개수를 함께 표시하는 경우가 많다
  • 조건에 해당하는 데이터 개수를 구하기 위해서는 count 함수를 써야된다
  • 예를 들어, 이름에 특정 단어가 포함된 회원 목록 조회하려면 다음 두 쿼리 써야된다.
SELECT id, ...
FROM member
WHERE name LIKE '지은%'
ORDER BY id DESC
LIMIT 20;
 
SELECT COUNT(*)
FROM member
WHERE name LIKE '지은%';
  • 데이터 적을땐 노상관이지만 많아지면 조건에 해당하는 모든 데이터 탐색해야된서 느려진다
  • 커버링 인덱스를 써도 전체 인덱스를 스캔해야되며, 커버링 인덱스가 아닌 경우에는 실제 데이터를 전부 읽어야됨
  • 해결방법은? 바로 소통!

오래된 데이터 삭제 및 분리 보관하기

  • 과거 데이터를 삭제할 수 있는 예로 로그인 시도 내역을 들 수 있다
  • 새로운 지역에서 로그인 시도 발생 탐지같은걸 개발하려면 로그인 시도 내역을 일정 기간 보관해야함
  • 로그인 시도 내역같은건 장기간 보관이 필요없다
    • 이상 징후 탐지를 위한거라 최근 몇달치만 있어도 충분

별도 캐시 서버 구성하기

  • 캐시 도입시 코드를 수정해야하지만, 코드 수정에 드는 비용대비 캐시로 증가시킬 수 있는 처리량이 크다면...
  • 코드를 수정하는것이 더 합리적이다.