목록 화면이 느렸다. 쿼리를 보니 두 개가 나간다.
SELECT * FROM orders WHERE ... ORDER BY id DESC LIMIT 20;
SELECT COUNT(*) FROM orders WHERE ...;
각각의 시간을 재 보니 목록은 빠르고 카운트가 느렸다.
Table of contents
Open Table of contents
카운트가 왜 느린가
목록 쿼리는 LIMIT 20 이 있어서 조건에 맞는 행을 20개 찾으면 멈춘다. 인덱스를 타면 금방 끝난다.
COUNT(*) 는 멈출 수 없고 몇 개인지 알려면 전부 세야 하므로 20만 건이면 다 본다.
그래서 목록보다 COUNT(*) 가 훨씬 오래 걸렸고 자료가 많아질수록 차이가 벌어진다.
목록이 느려지는 원인이 목록이 아니라 옆에 붙은 숫자였다. 같은 WHERE 인데 시간이 크게 달랐다.
왜 세고 있었나
화면을 봤더니 페이지네이션에 이렇게 나오고 있었다.
1 2 3 4 5 ... 8,241 총 164,820건
마지막 페이지 번호와 총 건수를 보여 주려고 COUNT(*) 를 돌리는데 그것이 실제로 필요한지를 물었다.
뒤쪽 페이지로 가는 사람이 있는지를 access_log 로 봤더니 거의 없었다. 총 건수가 정확해야 하는지도 물었는데 대략만 알면 되는 화면이었다.
필요 없는 것을 위해 매번 20만 건을 세고 있었다. 빠르게 만들기 전에 필요한지를 물었어야 했다.
선택지 — 세 가지 방법
첫째는 다음 페이지가 있는지만 확인하는 것이다.
SELECT * FROM orders WHERE ... ORDER BY id DESC LIMIT 21;
LIMIT 21 로 한 개 더 가져와 21개가 오면 다음 페이지가 있고 20개 이하면 마지막이다. 화면은 이전 과 다음 만 남는다.
총 건수와 마지막 페이지가 사라지는 대신 추가 쿼리가 아예 없다. 둘째는 대략적인 값을 쓰는 것이다.
SELECT table_rows FROM information_schema.tables
WHERE table_name = 'orders';
table_rows 는 빠르지만 부정확하고 WHERE 가 걸린 카운트에는 못 쓴다. 셋째는 조건별 카운트를 짧게 캐시하는 것이다.
셋째가 실제로 효과가 컸다. 사람들이 같은 WHERE 로 페이지를 넘기므로 한 번 세고 재사용한다.
셋을 섞었다
화면 성격에 따라 다르게 갔다.
관리자 목록 총 건수를 보여줘야 했다 → 조건별 카운트를 60초 캐시
사용자 목록 다음 페이지 유무만 확인하는 방식으로 변경
보고서 정확한 수가 필요하고 자주 안 본다 → 그대로 둠
사용자 목록은 총 건수를 안 보여 줘도 불만이 없었고 보고서는 하루 몇 번 안 도는 화면이라 안 건드렸다.
한 방식으로 통일하지 않은 것이 요점이다. 화면마다 그 숫자가 하는 일이 달랐다.
기술을 먼저 고르고 화면에 적용하면 이 구분이 안 나온다. 화면이 무엇을 필요로 하는지를 먼저 보면 세 방법이 각각 자기 자리를 찾는다.
정렬도 같이 봤다
카운트를 손보면서 정렬 쿼리도 봤다.
ORDER BY created_at DESC LIMIT 20
created_at 에 인덱스가 없어서 조건에 맞는 것을 다 찾아 정렬한 다음 앞의 20개를 취하고 있었다.
인덱스를 넣으니 그 순서대로 20개만 읽고 끝났고 같은 쿼리인데 비용 차이가 컸다.
다만 조건에 맞는 행이 20보다 적으면 어차피 전부 읽는다. 그때는 인덱스가 있어도 스캔과 비슷하므로 LIMIT 이 있다고 늘 싸지는 않다.
전체 흐름 — 확인 순서
목록이 느리면 이 순서로 본다.
1. 쿼리를 나눠서 각각 잰다 — 목록과 카운트 중 어느 쪽이 느린지
2. 카운트가 느리면 그게 정말 필요한지 묻는다 — 화면과 사용 로그를 본다
3. 필요하면 캐시하고 아니면 없앤다
4. 정렬 컬럼에 인덱스가 있는지 본다
둘째를 빼먹기 쉬운데 빠르게 만들 방법부터 찾게 되지만 안 하는 것이 가장 빠르다.
이번에 가장 크게 줄인 것도 COUNT(*) 를 안 나가게 만든 부분이다. 캐시나 인덱스는 그다음이었다.
정리
LIMIT이 있는 목록 쿼리는 빨리 끝나지만 카운트는 전부 세야 해서 느리다- 자료가 많아질수록 두 조회의 차이가 벌어진다
- 목록이 느린 원인이 목록이 아니라 옆에 붙은 숫자일 수 있다
- 총 건수가 정말 필요한지 접근 로그로 먼저 확인한다
LIMIT 21로 한 개 더 가져오면 추가 쿼리 없이 다음 페이지 유무를 안다table_rows는 빠르지만 조건이 걸린 카운트에는 못 쓴다- 같은 조건으로 페이지를 넘기므로 조건별 캐시가 잘 먹는다
- 화면마다 그 숫자가 하는 일이 달라 한 방식으로 통일하지 않는다
ORDER BY ... LIMIT은 인덱스가 있으면 그만큼만 읽고 없으면 전부 읽는다- 성능 개선에서 안 하는 것이 가장 빠르다