일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 파이썬
- MSA
- Proxy
- 네트워크
- 알고리즘
- 자바
- Algorithm
- 운영체제
- C
- react
- IT
- mongoDB
- Java
- design pattern
- 백준
- OS
- 컴퓨터구조
- 자료구조
- MySQL
- Spring
- Data Structure
- JPA
- c언어
- Kafka
- 디자인 패턴
- Heap
- JavaScript
- redis
- spring webflux
- Galera Cluster
- Today
- Total
시냅스
페이징 성능 최적화 - No Offset 은 왜 빠를까? 본문
이 글에서는 No Offset 방식이 왜 빠른지 이해하기 위한 개념과 예시를 상술합니다.
LIMIT
MySQL 에서 LIMIT 은 범위를 제한할 때 사용합니다.
쿼리 결과에서 지정된 순서에 위치한 레코드만 가져오고 싶을 때에 유용하게 사용됩니다.
SELECT *
FROM test_tb
LIMIT 0, 10;
위와 같은 쿼리에서 만약 LIMIT 이 없었다면 테이블 풀 스캔을 실행하여 결과값을 반환했을 것입니다.
하지만 이 때 LIMIT 을 사용하면서 MySQL 엔진은 레코드를 10건만 읽은 직후 반환하게 됩니다.
따라서 모든 레코드를 읽어야하는 것 보다 훨씬 부하가 줄게 됩니다.
-- 1
SELECT *
FROM test_tb
GROUP BY first_name
LIMIT 0, 10;
-- 2
SELECT *
FROM test_tb
WHERE emp_no between 10001 and 11000
ORDER BY first_name
limit 0, 10;
다만 위와 같이 단순히 LIMIT 만 걸려있는 것이 아니라,
GROUP BY, ORDER BY 등 어떤 조건이 있다면 상황은 달라집니다.
1번 쿼리는 테이블에서 모든 쿼리를 읽고 group by 를 처리한 이후 limit 을 처리합니다.
2번 쿼리는 where 절에 일치하는 레코드를 읽은 후 first_name 컬럼 값으로 정렬하고
이때 정렬하며 10건이 완성되면 결과를 반환합니다.
위의 두가지 경우는 모든 레코드를 읽고 후처리를 한 이후 결과값을 반환하기 때문에
LIMIT 을 사용하며 얻을 수 있는 성능적 향상을 취하기 어렵습니다.
따라서 LIMIT 을 효율적으로 사용하기 위해서는 옵티마이저의 실행 계획을 파악할 필요가 있습니다.
SELECT 절의 처리 순서
위의 순서는 각 요소가 없는 경우는 가능하지만 순서가 바뀌어서 실행되는 형태는 거의 없다고 봐도 무방합니다.
ORDER BY 나 GROUP BY 절이 있더라도 인덱스를 이용해 처리될 때는 그 단계 자체가 불필요하므로 생략됩니다.
또한 LIMIT 은 모든 절차를 마치고 난 이후에야 적용이 됩니다.
따라서 각 단계에서 인덱스를 적절히 사용하거나,
이미 사용하고 있다면 절차 자체를 줄여주는 노력이 필요합니다.
아래에서 예시로 살펴보겠습니다.
[ 참고 글, Covering Index ] 위와 관련된 모든 내용을 담고있진 않지만, 읽어보시면 도움이 될만한 내용입니다.
select *
from test_tb
order by id desc
limit 800000, 10;
--// => 10 rows fetched 280ms
위의 쿼리는 일반적으로 잘 사용하는 형태의 페이징 쿼리입니다.
현재 테이블에는 100만 개의 데이터를 준비해두었고,
80만 부터 10개의 row 를 가져오는 데에 260ms 가 걸렸습니다.
앞서 살펴본 실행계획에 따르면, 모든 레코드를 테이블에서 가져와 id 를 기준으로 역순 정렬하고
그 이후 80만까지 읽고 추가로 10개를 더 읽은 이후 이전의 80만개는 버려 10 개만 반환했을 것입니다.
그렇다면 위의 페이징 쿼리를 사용하며 레코드를 가져올 때 가져올 때
항상 이전의 모든 데이터를 읽고 버리고 10개만 반환하겠구나 라고 예상할 수 있습니다.
이러한 비효율을 개선하기 위해 No Offset 방식을 이용합니다.
No Offset
No Offset 은 Offset이 없는 페이징 쿼리입니다.
시작해야하는 조건을 offset 이 아닌 다른 방식으로 처리하여 모든 레코드를 읽지 않고 10개만 읽어 반환하여
시작 위치가 어디든 첫 페이지를 읽는 것과 동일한 효과를 내게 해주는 기법입니다.
아래에서 예시를 통해 살펴보겠습니다.
select *
from test_tb
where id <= 1000000 - 800000
order by id desc
limit 10;
--// => 10 rows fetched 2ms
조건은 No Offset 을 사용하지 않았던 쿼리와 마찬가지로 80만 에서 시작해서 10개만 찾아오는 것입니다.
현재 쿼리에서 id 는 pk, 인덱스로 관리됩니다.
실행계획을 토대로 살펴보면 우선 id 가 20만 이하인 레코드를 찾아왔을 것입니다.
이후 id 를 토대로 역순 정렬을 하면서 10개가 찾아졌다면 바로 반환했을 것입니다.
따라서 No Offset 을 사용하지 않았던 쿼리에서는 80만 + 10 모두정렬 후 10개를 반환했다면,
No Offset 방식은 order by 를 수행하는 동시에 id 가 인덱스이므로 역순으로 읽어 나가다가
result set이 10개가 되는 순간 바로 반환했을 것입니다.
No Offset 방식을 실행계획으로 확인해본 결과입니다.
예상한 대로 Using where 를 통해 시작 위치를 index(id) 를 사용하여 탐색했고
이후 range scan 을 했다는 것을 알 수 있습니다.
또한 rows 도 80만 개가 아닌 43만개로 추정되는 것을 확인할 수 있습니다.
(rows 는 추정치이므로 실제 반영된 결과는 아닙니다. 다만 80만보다는 절반에 가까운 수치임을 알 수 있습니다.)
SELECT 절의 실행 계획과 페이징에 대한 최적화를 예시와 함께 살펴보았습니다.
살펴보았듯, No Offset 방식은 페이징 쿼리의 성능을 크게 향상시킬 수 있는 효율적인 방법입니다.
이 방식을 통해 불필요한 데이터 읽기를 최소화하고, 인덱스를 효과적으로 활용하여 쿼리 처리 시간을 줄일 수 있습니다.
끝!
'데이터베이스 > MySQL' 카테고리의 다른 글
GTID 와 Galera Cluster (1) | 2023.10.09 |
---|---|
검색기능 개발 (MySQL Full-Text Index, Search) (0) | 2023.08.03 |
MySQL 커버링 인덱스 Covering Index (2) | 2023.05.29 |
MySQL 단편화 Fragmentation 의 이해와 해결 (2) | 2023.05.13 |
MySQL Repeatable Read 격리 수준의 트랜잭션 이해와 예제 (0) | 2023.05.10 |