일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- N+1
- JWT 쓰는 방법
- 레포지토리
- 스테이지어스
- 알림생성
- 3계층구조
- secret코드
- element.style
- .env
- JWT 쓰는이유
- getComputedStyle
- Winston
- JSON Web Token
- 환경변수
- route 53
- 게시글 이미지 업로드
- 메뉴바
- N+1문제
- unnest
- 쿼리스트링
- 메뉴바 한번에
- 네비게이션 한번에
- 게시글 이미지
- 포트번호
- 패스파라미터
- JWT
- 알림생성모듈
- 부트캠프
- 이미지가 포함된 게시글
- 토큰
- Today
- Total
기주
N+1 문제 해결하기 본문
프로젝트를 진행하던 중, 위키를 수정할때마다 기존에 수정한 이력이 있는 사용자들 모두에게 알림을 보내는 기능을 구현
해야했다. 기존에는 SELECT문으로 해당 위키를 수정한 사람들의 목록을 출력하고 그 숫자만큼 반복문으로, 알림을 추가
하는 Insert 쿼리를 돌려주었다. 그렇게 N+1 문제를 안게되었다.
N+1 문제란
SELECT문으로 나온 데이터의 개수만큼 다른 쿼리문(update, insert, delete)을 반복하는 경우를 말한다.
그래서 실제로는 1+N에 더 가깝다고 볼 수 있다.
문제가 되는 이유)
그래서 이게 왜 문제가되냐면, DB에 연결하는 작업은 프로그램에서 가장 느린 작업이다. 그런데 SELECT문으로 가져온
데이터의 결과수 만큼 새로운 쿼리문을 반복한다는 것은 가져온 데이터의 수가 몇개인지도 예측할 수 없는 상황에서 그만
큼 <DB연결 + DB작업>을 하는 것은 언제 해당 작업이 끝날지 알 수 없다.
특히 해당 기능은 이전에 해당 위키를 수정한 이력이 있는 사용자들 모두에게 알림을 보내는 기능이다.
해당 기능이 수행될때마다 계속해서 누적되어 더욱 무거워지는 작업이고, 만약 1001번째 작업자가 수정하고, 연달아 2번
째, 3번째 사람도 추가적으로 수정한다면 순식간에 3000천개의 데이터가 생성되고 단순 반복문으로 이를 시도한다면
3000번 DB연결 + 3000번 쿼리문실행으로 아주 쉽게 서버가 다운될 것이다.
이를 지난시간에 배운 Bulk Insert와 unnest를 이용하여 해결해주었다.
기존의 잘못된코드)
//기존 게임수정자들 추출
const historyUserSQLResult = await poolClient.query(
`SELECT DISTINCT
user_idx
FROM
history
WHERE
game_idx = $1`,
[gameIdx]
);
let historyUserList = historyUserSQLResult.rows; // SELECT문의 결과리스트
// SELECT문(1) 결과(N)만큼 쿼리문실행 (1+N문제)
for(int i=0; i<historyUserList.length; i++) {
await generateNotification(poolClient, 'MODIFY_GAME', gameIdx, historyUserList[i]);
}
수정된코드)
//기존 게임수정자들 추출
const historyUserSQLResult = await poolClient.query(
`SELECT DISTINCT
user_idx
FROM
history
WHERE
game_idx = $1`,
[gameIdx]
);
let historyUserList = historyUserSQLResult.rows; // SELECT문의 결과리스트
await generateNotifications({
conn: poolClient,
type: 'MODIFY_GAME',
gameIdx: gameIdx,
//SELECT문의 결과로 받아온 리스트들을 user_idx만 뽑아 다시 리스트로만들고 이 리스트를
//알림생성모듈의 매개변수로 전달
toUserIdx: historyUserList.map((elem) => elem.user_idx),
});
수정된코드) - 알림생성모듈에 unnest 적용)
//Bulk Insert와 unnest를 적용하기
// unnest의 $2부분에 userIdx의 리스트가 들어가게되면서
// SELECT unnest로 리스트의 개수만큼, 데이터 행이 생성되고, insert문에 대입되는 구조
const generateNotifications = async (option) => {
(option.poolClient || pool).query(
`INSERT INTO
notification (type, game_idx, post_idx, user_idx)
SELECT
2, $1, NULL,
UNNEST($2::int[])`,
[option.gameIdx, option.toUserIdx]
);
};
//모듈 exports
module.exports = { generateNotification, generateNotifications };
이걸로 단1번의 DB연결과 쿼리문 실행으로 모두에게 알림을 보낼 수 있게되었다.
해당작업의 수행시간을 1/100이하로 줄인셈이다.
'TIL' 카테고리의 다른 글
[DB] 트랜잭션 동시에 실행해서 성능올리기 ( confilct serializable한 nonserial schedule을 허용하기 ) (0) | 2024.04.08 |
---|---|
[DB] soft delete 논리삭제 (0) | 2024.04.05 |
3계층구조) (0) | 2024.04.05 |
[DB] DB에서 특정한 형식으로 출력하기(시간, 문자열) (TO_CHAR()이용하기), 타임존 설정하기 (0) | 2024.04.05 |
페이지네이션 & 무한스크롤 구현하기 (0) | 2024.04.03 |