-
N+1 문제 해결하기TIL 2024. 4. 5. 22:06
프로젝트를 진행하던 중, 위키를 수정할때마다 기존에 수정한 이력이 있는 사용자들 모두에게 알림을 보내는 기능을 구현
해야했다. 기존에는 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