채팅 100만개, 유저 10만개
페이지네이션을 위해 채팅을 20개씩 가져오기
채팅 정보와 함께 보낸 이 정보를 같이 반환
EXPLAIN ANALYZE
SELECT user.chatName, chat.stringContent
FROM user
JOIN chat ON user.id = chat.senderId
WHERE chat.masterId = 2
AND chat.updated_at < '2025-02-28 15:01:30'
ORDER BY chat.updated_at DESC
LIMIT 20;
mysql> EXPLAIN ANALYZE
-> SELECT user.chatName, chat.stringContent
-> FROM user
-> JOIN chat ON user.id = chat.senderId
-> WHERE chat.masterId = 2
-> AND chat.updated_at < '2025-02-28 15:01:30'
-> ORDER BY chat.updated_at DESC
-> LIMIT 20;
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| -> Limit: 20 row(s) (cost=616088 rows=20) (actual time=223..223 rows=20 loops=1)
-> Nested loop inner join (cost=616088 rows=493698) (actual time=223..223 rows=20 loops=1)
-> Index range scan on chat using idx_masterId_updated_at over (masterId = 2 AND updated_at < '2025-02-28 15:01:30.000000') (reverse), with index condition: ((chat.masterId = 2) and (chat.updated_at < TIMESTAMP'2025-02-28 15:01:30')) (cost=107521 rows=493698) (actual time=222..222 rows=20 loops=1)
-> Single-row index lookup on user using PRIMARY (id = chat.senderId) (cost=0.93 rows=1) (actual time=0.0503..0.0507 rows=1 loops=20)
|
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.35 sec)
전체 쿼리 실행:
Nested loop inner join:
chat 테이블 접근:
user 테이블 조인:
- Single-row index lookup on user using PRIMARY
- 실행 횟수: 20 loops
- 실제 실행 시간: 0.0503ms에서 0.0507ms per loop
분석:
인덱스 효율성: idx_masterId_updated_at 인덱스가 효과적으로 사용되고 있습니다.
조인 효율성: user 테이블과의 조인이 매우 효율적으로 수행되고 있습니다.
LIMIT의 효과: 20개의 행만 필요하므로, 전체 493698개의 예상 행 중 실제로는 20개만 처리됩니다.
실행 시간: 전체 쿼리 실행 시간이 223ms로, 상대적으로 빠른 편입니다.
| 5 | 0.04615650 | SELECT user.chatName, chat.stringContent
FROM user
JOIN chat ON user.id = chat.senderId
WHERE chat.masterId = 2
AND chat.updated_at < '2025-02-28 15:01:30'
ORDER BY chat.updated_at DESC
LIMIT 20 |
0.04 초 소요!!
chat을 조건에 맞는거 20개 가져오고나서, 그다음에 user과 조인하기
SELECT user.chatName, c.stringContent
FROM (
SELECT senderId, stringContent
FROM chat
WHERE masterId = 2
AND updated_at < '2025-02-28 15:01:30'
ORDER BY updated_at DESC
LIMIT 20
) AS c
JOIN user ON user.id = c.senderId;
mysql> EXPLAIN ANALYZE
-> SELECT user.chatName, c.stringContent
-> FROM (
-> SELECT senderId, stringContent
-> FROM chat
-> WHERE masterId = 2
-> AND updated_at < '2025-02-28 15:01:30'
-> ORDER BY updated_at DESC
-> LIMIT 20
-> ) AS c
-> JOIN user ON user.id = c.senderId;
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| -> Nested loop inner join (cost=107575 rows=20) (actual time=2.46..2.59 rows=20 loops=1)
-> Table scan on c (cost=107552..107554 rows=20) (actual time=2.45..2.48 rows=20 loops=1)
-> Materialize (cost=107552..107552 rows=20) (actual time=2.44..2.44 rows=20 loops=1)
-> Limit: 20 row(s) (cost=107547 rows=20) (actual time=2.38..2.41 rows=20 loops=1)
-> Index range scan on chat using idx_masterId_updated_at over (masterId = 2 AND updated_at < '2025-02-28 15:01:30.000000') (reverse), with index condition: ((chat.masterId = 2) and (chat.updated_at < TIMESTAMP'2025-02-28 15:01:30')) (cost=107547 rows=493698) (actual time=2.38..2.39 rows=20 loops=1)
-> Single-row index lookup on user using PRIMARY (id = c.senderId) (cost=0.935 rows=1) (actual time=0.00247..0.00293 rows=1 loops=20)
|
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.01 sec)
전체 쿼리 실행:
서브쿼리 (c) 실행:
서브쿼리 내부 실행:
user 테이블 조인:
- Single-row index lookup: PRIMARY 키를 사용하여 각 행마다 정확히 한 번의 조회를 수행합니다.
- 예상 비용: 0.935, 예상 행 수: 1
- 실제 실행 시간: 0.00247ms에서 0.00293ms, 실제 행 수: 1
- 20번 반복 (loops=20): 서브쿼리의 각 결과에 대해 수행됩니다.
분석:
인덱스 효율성: idx_masterId_updated_at 인덱스가 매우 효과적으로 사용되고 있습니다. 493698개의 행 중 필요한 20개만 빠르게 찾아냅니다.
LIMIT의 효과: 서브쿼리에서 20개의 행만 선택하여 전체 쿼리 성능을 크게 향상시킵니다.
조인 효율성: user 테이블과의 조인이 매우 효율적으로 수행되고 있습니다. 각 조인마다 단 한 번의 인덱스 조회만 필요합니다.
전체 실행 시간: 약 2.59ms로 매우 빠른 실행 시간을 보여줍니다.
WITH chat_subset AS (
SELECT senderId, stringContent
FROM chat
WHERE masterId = 2
AND updated_at < '2025-02-28 15:01:30'
ORDER BY updated_at DESC
LIMIT 20
)
SELECT u.chatName, c.stringContent
FROM chat_subset c
JOIN user u ON u.id IN (SELECT DISTINCT senderId FROM chat_subset)
WHERE u.id = c.senderId;
mysql> EXPLAIN ANALYZE
-> WITH chat_subset AS (
-> SELECT senderId, stringContent
-> FROM chat
-> WHERE masterId = 2
-> AND updated_at < '2025-02-28 15:01:30'
-> ORDER BY updated_at DESC
-> LIMIT 20
-> )
-> SELECT u.chatName, c.stringContent
-> FROM chat_subset c
-> JOIN user u ON u.id IN (SELECT DISTINCT senderId FROM chat_subset)
-> WHERE u.id = c.senderId;
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| -> Nested loop inner join (cost=107605 rows=400) (actual time=0.395..0.503 rows=20 loops=1)
-> Nested loop inner join (cost=107565 rows=20) (actual time=0.385..0.422 rows=6 loops=1)
-> Table scan on <subquery3> (cost=107559..107562 rows=20) (actual time=0.372..0.381 rows=6 loops=1)
-> Materialize with deduplication (cost=107559..107559 rows=20) (actual time=0.369..0.369 rows=6 loops=1)
-> Table scan on chat_subset (cost=107552..107554 rows=20) (actual time=0.311..0.34 rows=20 loops=1)
-> Materialize CTE chat_subset if needed (cost=107552..107552 rows=20) (actual time=0.306..0.306 rows=20 loops=1)
-> Limit: 20 row(s) (cost=107547 rows=20) (actual time=0.247..0.276 rows=20 loops=1)
-> Index range scan on chat using idx_masterId_updated_at over (masterId = 2 AND updated_at < '2025-02-28 15:01:30.000000') (reverse), with index condition: ((chat.masterId = 2) and (chat.updated_at < TIMESTAMP'2025-02-28 15:01:30')) (cost=107547 rows=493698) (actual time=0.244..0.255 rows=20 loops=1)
-> Single-row index lookup on u using PRIMARY (id = `<subquery3>`.senderId) (cost=1.03 rows=1) (actual time=0.0034..0.00386 rows=1 loops=6)
-> Index lookup on c using <auto_key0> (senderId = `<subquery3>`.senderId) (cost=107552..107552 rows=2) (actual time=0.0035..0.00852 rows=3.33 loops=6)
-> Materialize CTE chat_subset if needed (query plan printed elsewhere) (cost=107552..107552 rows=20) (never executed)
|
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
전체 쿼리 실행:
CTE (chat_subset) 실행:
DISTINCT senderId 처리:
user 테이블 조인:
최종 결과 조합:
- Index lookup on c using <auto_key0>
- 실행 횟수: 6 loops
- 실제 실행 시간: 0.0035ms에서 0.00852ms per loop
- 평균 행 수: 3.33 rows per loop
분석:
인덱스 효율성: idx_masterId_updated_at 인덱스가 효과적으로 사용되어 초기 20개 행을 빠르게 선택합니다.
CTE 사용: chat_subset CTE를 사용하여 초기 데이터를 효율적으로 제한합니다.
DISTINCT 처리: 20개의 채팅에서 6개의 고유한 senderId를 효율적으로 추출합니다.
조인 효율성: user 테이블과의 조인이 매우 효율적으로 수행되고 있습니다.
전체 실행 시간: 약 0.503ms로 매우 빠른 실행 시간을 보여줍니다.
| 방식 | 실행 시간 | 실제 행 수 | 예상 행 수 | 인덱스 사용 | 특징 |
|---|---|---|---|---|---|
| 1. Join | 223ms | 20 | 493,698 | idx_masterId_updated_at | Nested loop inner join 사용 |
| 2. 인덱스 추가 | 46ms | - | - | - | 인덱스 추가로 성능 크게 향상 |
| 3. 서브쿼리 | 2.59ms | 20 | 20 | idx_masterId_updated_at | Nested loop inner join, Materialize 사용 |
| 4. CTE와 IN | 0.503ms | 20 | 400 | idx_masterId_updated_at | Nested loop inner join, Materialize with deduplication 사용 |
Materialize
Materialize는 쿼리 실행 과정에서 중간 결과를 임시로 저장하는 기법입니다. 이는 다음과 같은 이점을 제공합니다:
CTE (Common Table Expression)
CTE는 복잡한 쿼리를 더 읽기 쉽고 관리하기 쉽게 만드는 SQL 기능입니다. 주요 특징은 다음과 같습니다:
WITH 키워드로 시작하여 정의됩니다.| 특성 | DB에서 IN으로 검색 | Backend에서 map으로 검색 |
|---|---|---|
| 네트워크 통신 | 단일 쿼리로 통신 횟수 감소 | 여러 번의 쿼리로 통신 횟수 증가 가능 |
| 인덱스 활용 | DB 인덱스 효율적 활용 가능 | 개별 쿼리마다 인덱스 활용, 비효율적일 수 있음 |
| 쿼리 복잡성 | 복잡한 쿼리 작성 필요 가능 | 간단한 쿼리 유지 가능 |
| DB 부하 | 단일 쿼리로 부하 집중 가능 | 여러 쿼리로 부하 분산 가능 |
| 코드 복잡성 | DB 쿼리에 로직 집중 | 애플리케이션 코드에 로직 분산 |
| N+1 문제 | 회피 가능 | 발생 가능성 높음 |
💡 그 당시에는 Backend에서 처리하는게 훨씬 빠르다고 느꼈지만, sql에 관해 공부하고, 직접 explain analzye 쳐보고 비교하면서 DB에서 IN으로 처리하는게 좋다고 생각한다.
그리고 N+1쿼리 문제 발생하지 않고 DB 접근을 한번에 해서 더 수월하다!